1. Introduction
JPickList is a JQuery based Picklist plugin. It can be used to display PickList for selection if Items (like index of a textBook). JPickList can also be used for layered display of hierarchical data.
Below is how one of the PickLists in our Samples look like.
In the attached source code zip, there are two picklists - first is the common Alphabetical (textbook Index like) picklist of Cities (screenshot above). The second picklist is a sample hierarchial data display (CalendarPicker). Also there are two JPickList javascripts included (jquery.jpicklist.js(JQuery Plugin version) and JPickList.js (simplified version for understanding)).
2. Background
There are lots of scenarios where Indexes are very useful to quickly find and select the information we are looking for (for example in textbooks). Also, despite the auto-complete facility - there are times when we would want to provide our Web Application user some initial hint to get started - or a facility to pick the information user is looking for. We try to solve this problem using JPicklist as in diagram above.
Another good use of this utility is in layered display of hierarchical data. The second example in the attached source code, displays a sample hierarchical data - the index(top menu) first displays Months, then display Days in corresponding month and finally display TimesSlots for the day in content panel which can be selected. As the Index (top menu) is rendered dynamically, we can present hierarchical data dynamically updating the top menu as per hierarchy. ( If we want only content rather than selection list - we can simply override the content template. We can then just bypass the setupCheckboxesJPickList function as we only want to display content not check-boxes.)
Dependency:-
- This plugin is written in Javascript and requires JQuery and Handlebar.js.
- For display of popup I have used JQuery UI dialog. This functionality is however, completely outside the JPicklist plugin. The JPicklist plugin by default simply builds the Picklist into a div and hides it. We can then render this Div in whichever form we want (e.g. we can simply render it in some area/section in the page and it would still work.) In our sample code I am launching the div in a Modal Dialog.
- I am using .Net here it not necessarily needs to be .Net server side. As long as we are getting JSON formatted in the Model below - the plugin should work. The front end has no dependency on .Net.
If we want we can simply show/hide the div in some area in the page. To test the feature just uncomment the //$('#JPickListDiv').show(); and $('#JPickListDiv').hide(); code and comment out the .dialog("....") code in the Page HTML and see it working.
3. Using the code
3.1 Configure the View/HTML:-
Following javascripts & css should be included:-
- 1. JQuery (e.g. <script src="/Scripts/jquery-1.10.2.min.js"></script>)
- 2. Handlebar (<script src="/Scripts/handlebars.js"></script>)
- 3. jquery.JPicklist.js or JPicklist.js (<script src="/Scripts/jquery.jpicklist.js"></script>)
- 4. JPickList.css ( <link href="/Content/JPickList.css" rel="stylesheet" />)
I am using JQuery UI to render the picklist into a dialog. As mentioned earlier, rendering the div in Popup or some page section is entirely up to our preference. We can instead use jQModal or any other plugin to display popup. Below is the JQuery UI Scripts I have used to display the popup.
- 5. JQuery UI javascript (<script src="/Scripts/jquery-ui.js"></script>)
- 6. JQuery UI Css (<link href="/Content/jquery-ui.min.css" rel="stylesheet" />, <link href="/Content/jquery-ui.theme.css" rel="stylesheet" /> etc.)
I have used the default bootstrap css provided by Visual Studio environment to render the page. (This is just for convenience, we can obviously use our own CSS for page layout.)
The JPickList is basically contained in two files "JPickList.css" and "jquery.jpicklis.js" (or "JPickList.js" if we prefer). JPickList.css contains all the styles for the JPicklist container. jquery.jpicklist.js (or JPickList.js) is the Javascript which contains the implementation of JPickList. The JS we have not minified. We can do it simply by either using .Net optimizations or grunt. For here we will just keep it simple. We can minify it using many tools available online.
3.1.a Configure Jquery plugin
Jquery plugin version is very much as regular jquery plugin works. We attach it to any element by calling it like this.
$('#JPickListDiv').JPickList({ postUrl:'/Home/AllCitiList', onItemsAdded: addItemToPage, onCancel: handleOnCancel });
3.1.b Simplified plugin
Here we are a bit more explicit. We will need to provide the ID of our JPickListDiv (so that JQuery can select it) along with other parameters. I have included this version to help in understanding the plugin. Once this version was working already - we simply converted it to JQuery plugin version above.
JPickList.init({ JPicklistDivSelector: '#JTimeSlotsPickListDiv', postUrl: '/Home/CalendarPicker', onItemsAdded: addItemToPage2, onCancel: handleOnCancel2 });
3.1.c Launch/Show the JPickList
The JPicklist we configured above will be ready in the Dom of our page however it will be hidden. All I need to do is just show the JPickList. As in our example we are launching the div in a JQuery dialog as below:-
$(_jPiclListSelector).dialog({ modal: true, height: 400, width: 520 }).show();
Or I can simply call show() on the div and it would still work:-
$('#JPickListDiv').show();
4. How JPickList Works
Below is the basic sequence diagram explaining the sequence of activities which the plugin performs to configure the JPickList for a particular DIV.
Let us take a look at each of the function of JPickList in sequence as mentioned in the diagram above:
4.1 Our ViewModel (JSON format that we post and receive from server)
The ViewModel (or Model) of our Plugin that we receive from Server (as JSON) is as below. I am using .Net here it not necessarily needs to be .Net server side, it can be any backend java/PHP/Python etc as long as we get the JSON in the below format, we are fine.
Here is the Model of the data we receive from the Server.
{
Alphabet: "a",
IsFirstLoad: false,
ItemsCount: 565,
ItemsList: [{Key:AAH, Value:Aachen, IsSelected:false, DisplayText:Aachen},…],
PagingKeysList: [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z],
SelectedKeys: "["HBL","BFJ"]"
}
Model of each display item in ItemsList is as below:-
{Key:AAH,
Value:Aachen,
IsSelected:false,
DisplayText:Aachen}
Quite straightforward, We have the current Alphabet. IsFirstLoad is optional boolean parameter it is set when we first load the list. Selectedkeys is a list of string it is the set of keys we have selected in our picklist. Then we have ItemsList which our JPickList displays in Content DIV for us to pick items. The model of JPickListItem is shown above. PickListItem is very much a simple KeyValue dictionary with some additional properties. DisplayText is optional if in certain scenario our display text is different from the Value.
So below are the corresponding classes (.Net in our example) which we fill up - serialize to JSON - and return on each request from server.
public class JPickerListItems
{
public string Key { get; set; }
public string Value { get; set; }
public bool IsSelected { get; set; }
public string DisplayText { get; set; }
}
public class JPickerListViewModel
{
public List<string> PagingKeysList { get; set; }
public List<JPickerListItems> ItemsList { get; set; }
public string Alphabet { get; set; }
public bool IsFirstLoad { get; set; }
public int ItemsCount { get; set; }
public string SelectedKeys { get; set; }
public List<string> _SelectedKeys {
get
{
if (!string.IsNullOrWhiteSpace(SelectedKeys))
{
return (SelectedKeys.Split(new char[] { ',' }).Select(x => x.Trim(new char[] { '"', ' ', '\\' })).ToList());
}
else
{
return (null);
}
}
}
}
Rest of the server side code is just filling the above Data model according to the request and returning it. In the first example we are simply pulling the list from a text file and filtering it according to request. In the second example we are just constructing the dummy data with simple code-behind.
I am using .Net here just for convenience. The plugin obviously should work regardless of .Net platform. Server side can be running PHP/Java/Python/Node.js or any other environment. As long as we are getting JSON in the format outlined above - we are good to go.
4.2 JPickList function (or the init function in simplified version):
Once we call the plugin the first workspace is the root (JPickList) function. As everything is an Object in Javascript - so as our plugin. Therefore for every unique instance of the plugin we want to create - we create a local copy of the "settings" to be used. This local copy is created by merging the "options" (supplied to our plugin while initialization) - with the local "_defaults". Thereon we simply pass this local copy of the "settings" for rest of our plugin configuration to work upon. Below is the JQuery helper function which helps us in making doing this:-
var settings = $.extend({}, this._defaults, options);
The Jquery helper function above helps us safely create a copy of merged local settings. We do not want to work on same copy of settings object every time we instantiate the JPicklist in the page - as in that case data and handlers from one JPicklist will overlap with data,handlers of other JPicklist. Above extend function creates a safe copy of "settings" for us to work on.
$.fn.JPickList = function(options) {
var _defaults = {
currentItems: [],
onItemsAdded: function (currentitems) { },
onItemsChanged: function (currentitems) { },
onCancel: function (currentitems) { },
clearOnCancel:false,
JPicklistDivSelector: '#JPickListDiv',
postUrl: ''};
if (!options.currentItems) {
options.currentItems = [];
}
var settings = $.extend({}, this._defaults, options);
var $t = $(this);
settings.JPicklistDivSelector = $t;
$.fn.JPickList.setupJPickList(settings);
$.fn.JPickList.buildJPickList(settings, {});
$.fn.JPickList.setupJPickListButtons(settings);
$(settings.JPicklistDivSelector).hide();
};
4.3 Setup JPickList HTML (setupJPickList)
The first thing we do is simply build the HTML frame of our JPickList. Below is the function we use to create the HTML. This is quite simple HTML. There is "jqmAlphabetPanelDiv" (top menu) , then there is the Contents Div (JPickListItemsDiv), then there is the div containing the two buttons Cancel and Add Items (JPickListButtons). Then we have a hidden field (hdnSelectedItemVals) to store our selected Keys inside each JPickList instance .
$.fn.JPickList.setupJPickList = function (settings) {
var jPicklistTemplate = '<div class="jqmAlphabetPanelDiv"></div>';
jPicklistTemplate += '<div class="JPickListItemsDiv"></div>';
jPicklistTemplate += '<div class="JPickListButtons">';
jPicklistTemplate += '<a href="javascript:void(0)" class="cancel"><span>Cancel</span></a>';
jPicklistTemplate += '<a href="javascript:void(0)" class="addItems"><span>Add Items</span></a>';
jPicklistTemplate += '</div>';
jPicklistTemplate += "<input type=\"hidden\" value='' class=\"hdnSelectedItemVals\" />";
$(settings.JPicklistDivSelector).html(jPicklistTemplate);
};
4.4 Build JPickList top menu and content (buildJPickList) *
Next we call this function (buildJPickList) - this is where all the communication with the server happens. We make an AJAX request to the server by building the appropriate ViewModel as outlined in the JSON Data Model format above.
Once we receive the response from server, this function then calls the two other important functions below to first configure the top menu (setupJPickListAlphaLinks) and then configure the content checkboxes (setupCheckboxesJPickList)
$.fn.JPickList.buildJPickList = function (settings, postdata) {
$.ajax({
url: settings.postUrl,
type: "POST",
data: postdata,
success: function (data) {
var alphaTmplt = '<ul class="tblJPickListAlphabets">{{#.}}<li><a href="{{.}}" class="JPickListAlphaLink">{{.}}</a></li>{{/.}}</ul>';
var templ = Handlebars.compile(alphaTmplt);
var alphHtml = templ(data.PagingKeysList);
$(settings.JPicklistDivSelector).children('.jqmAlphabetPanelDiv:first').html(alphHtml);
$.fn.JPickList.setupJPickListAlphaLinks(settings);
var templHtml = '<ul class="picklistItems">{{#ItemsList}}<li><label class="normaltextblue"><input type="checkbox" class="chkJPickList" {{#IsSelected}}checked{{/IsSelected}} value="{{Key}}"/> {{DisplayText}}</label></li>{{/ItemsList}}</ul>';
var template = Handlebars.compile(templHtml);
var htmlstr = template(data);
$(settings.JPicklistDivSelector).children('.JPickListItemsDiv:first').html(htmlstr);
$.fn.JPickList.setupCheckboxesJPickList(settings);
},
error: function (xhr, ajaxOptions, thrownError) {
$(settings.JPicklistDivSelector).children('.JPickListItemsDiv:first').html('Something went wrong. Please retry again! Error Details:'+JSON.stringify(xhr));
},
timeout: 7000
});
};
4.4.1 Setup the top menu Hyperlinks (setupJPickListAlphaLinks)
This function sets up the top menu Links. Please observe that we want to ensure that only one "click" event handler is attached to the hyperlinks - so that on every click only one buildJPickList is triggered. Otherwise it can trigger the AJAX request multiple times and we will have another handler bound to click again. That is why we unbind the 'click' (off('click', '.JPickListAlphaLink'))and bind it again (on('click', '.JPickListAlphaLink', function(....) - to make sure that we have single handler. Below is how our code for binding the top menu links looks like.
$.fn.JPickList.setupJPickListAlphaLinks = function (settings) {
$(settings.JPicklistDivSelector).off('click', '.JPickListAlphaLink').on('click', '.JPickListAlphaLink', function (event) {
event.preventDefault();
var _letter = $(this);
_letter.addClass("active");
var ur = $(this).attr('href');
var selItms = eval($(settings.JPicklistDivSelector).children('.hdnSelectedItemVals:first').val());
var pdata = { Alphabet: ur, SelectedKeys: JSON.stringify(selItms) };
$.fn.JPickList.buildJPickList(settings, pdata);
return false;
});
};
4.4.2 Setup the checkboxes in Content DIV (setupCheckboxesJPickList)
This function sets up the checkboxes in the Content Div. What we are doing is
- for every item selected - we push the item to an array.
- For every item de-selected - we pop it out of the array.
- we save it to the hidden field (hdnSelectedItemVals)
- We then call the (onItemsChanged) function if it was provided.
Other than that - on load we are just checking and unchecking the checkboxes based on the existing keys.
$.fn.JPickList.setupCheckboxesJPickList = function (settings) {
if ($(settings.JPicklistDivSelector).children('.hdnSelectedItemVals:first').val()) {
settings.currentItems = JSON.parse($(settings.JPicklistDivSelector).children('.hdnSelectedItemVals:first').val());
}
$(settings.JPicklistDivSelector).children('.chkJPickList').each(function () {
if ((settings.currentItems) && (settings.currentItems.length > 0)) {
var _val = $(this).val();
for (var tmp = 0; tmp < settings.currentItems.length; tmp++) {
if (_val === settings.currentItems[tmp]) {
this.setAttribute("checked", "");
this.checked = true;
}
}
}
});
$(settings.JPicklistDivSelector).off('click','.chkJPickList').on('click','.chkJPickList', function (ev) {
var _val = $(this).val();
if (this.checked) {
if ($.inArray(_val, settings.currentItems) == -1) {
settings.currentItems.push(_val);
}
$($(settings.JPicklistDivSelector).children('.hdnSelectedItemVals:first')).val(JSON.stringify(settings.currentItems));
}
else {
var itmidx = $.inArray(_val, settings.currentItems);
if ($.inArray(_val, settings.currentItems) > -1) {
settings.currentItems.splice(itmidx, 1);
$($(settings.JPicklistDivSelector).children('.hdnSelectedItemVals:first')).val(JSON.stringify(settings.currentItems));
}
}
if (settings.onItemsChanged) {
settings.onItemsChanged(settings.currentItems);
}
});
};
4.5 Configure the JPickList action buttons "Add Items and Cancel" (setupJPickListButtons)
This function simply attaches the appropriate handlers provided in the "options" while initializing JPickList. Following two handlers are called.
- ".addItems" link button calls "settings.onItemsAdded" and
- ".cancel" link button calls "settings.onCancel"
Below is how the code look like for binding the click event of these buttons (Hyperlinks decorated as buttons):-
$.fn.JPickList.setupJPickListButtons = function (settings) {
$(settings.JPicklistDivSelector).off('click', ' .JPickListButtons > .addItems').on('click', ' .JPickListButtons > .addItems', function () {
settings.onItemsAdded(settings.currentItems);
});
$(settings.JPicklistDivSelector).off("click", " .JPickListButtons > .cancel").on("click", " .JPickListButtons > .cancel", function () {
if (settings.clearOnCancel) {
$.fn.JPickList.clearValues(settings);
}
settings.onCancel(settings.currentItems);
});
};
4.6 Server side
(I am using .Net here just for convenience. Server side can be any language Java/PHP/Python/Node.js as long as we are getting JSON formatted in the Data Model outlined above - the JPickList plugin should work.)
We have two ViewModel classes ( as outlined above). For filling the Cities list, I am reading it from a text file.
private JPickerListViewModel LoadCities(JPickerListViewModel model)
{
model.ItemsList = new List<JPicklist.Controllers.JPickerListItems>();
model.ItemsCount = 0;
Dictionary<string, string> cd = JPicklist.Models.AirportCodes.Instance().ReadAllCodes();
var fltcd = cd.Where(item=> item.Value.Trim().StartsWith(model.Alphabet,StringComparison.InvariantCultureIgnoreCase));
foreach (var item in fltcd)
{
var itmToAdd = new JPickerListItems() { Key = item.Key, Value = item.Value, DisplayText = item.Value, IsSelected = false };
if (((model._SelectedKeys != null) && (model._SelectedKeys.Count > 0))&&(model._SelectedKeys.Contains(itmToAdd.Key)))
{
itmToAdd.IsSelected = true;
}
model.ItemsList.Add(itmToAdd);
model.ItemsCount++;
}
return model;
}
For filling the calendar, I am just creating it manually.
private JPickerListViewModel LoadTimeSlots(JPickerListViewModel model)
{
model.ItemsList = new List<JPicklist.Controllers.JPickerListItems>();
model.ItemsCount = 0;
if ((string.IsNullOrWhiteSpace(model.Alphabet)) || (model.Alphabet.Equals("back", StringComparison.InvariantCultureIgnoreCase)))
{
model.PagingKeysList = months.Select(x => x).ToList();
return model;
}
else if(months.Where(x=>x.Equals(model.Alphabet,StringComparison.InvariantCultureIgnoreCase)).Count()>0)
{
int numDays= daysinmonth[Array.IndexOf(months,model.Alphabet)];
model.PagingKeysList = new List<string>();
for (int tmp = 1; tmp <= numDays; tmp++)
{
model.PagingKeysList.Add(model.Alphabet+"-"+tmp.ToString());
}
model.PagingKeysList.Add("back");
}
else
{
model.PagingKeysList = new List<string>();
model.PagingKeysList.Add("back");
var itmlist = timeslots.Select(y => new JPickerListItems() { Key = model.Alphabet + "-" + y, Value = model.Alphabet + "-" + y, DisplayText = y, IsSelected = ((model._SelectedKeys != null) && (model._SelectedKeys.Count > 0) && (model._SelectedKeys.Contains(model.Alphabet+"-"+y))) }).ToList();
foreach (var item in itmlist)
{
model.ItemsList.Add(item);
}
}
return model;
}
5. History
13-Nov-2014: Initial Publish.