"We really like what we see but...there could be a lot of items on the screen. It would be really nice if a user entered a value in a text, and the text box stayed open. This would be a good indication to the user that they made a selection...a visual cue that they performed work."
Can we do this? Yes we can. The problem with this request is that now they are asking for a behavior that was not an intended behavior for the jQuery UI accordion. At times, we are at the mercy of the business and must give them what they want. This post will demonstrate how we can do this. Nevertheless, this is a very good exercise of creating a pseudo-accordion. It looks like a jQuery UI accordion, acts like a jQuery UI accordion, but there is no actual call to the jQuery accordion.
This example will be a little different from the previous as I will be showcasing a different screen mock up. Nevertheless, it's not much of a stretch. Let's dive in an start with our model class:
public class Item { public string ItemId { get; set; } public string ItemDescription { get; set; } public string PackQuantity { get; set; } public string PackSize { get; set; } }
Let's write the controller:
public class PanelController : Controller { public ActionResult Index() { return View(); } [HttpGet] public object GetItemData() { List<Item> items = new List<Item>() { new Item() { ItemId = "1", ItemDescription = "Item A", PackQuantity = "", PackSize = "5" }, new Item() { ItemId = "2", ItemDescription = "Item B", PackQuantity = "", PackSize = "10" }, new Item() { ItemId = "3", ItemDescription = "Item C", PackQuantity = "", PackSize = "15" } }; var data = items.GroupBy(x => x.ItemDescription) .Select(y => new { Headers = y.Key, Contents = y.Select(z => z).ToList() }); var json = JsonConvert.SerializeObject(data); return json; } }The interesting thing to note is the return type on the GetItemData method. Usually we would write return Json(data, JsonRequestBehavior.AllowGet);, above is just showing a different approach. Let's now write the view for the Index method. We will be doing this in steps, first the template, followed by the css, finally, the JavaScript.
@{ ViewBag.Title = "Panel"; } <br /> <div id="tmplTarget"></div> <script id="itemsTmpl" type="text/x-handlebars-template"> <div id="itemsContent"> {{#each this}} <div> <h3> <span class="ui-icon ui-icon-plus" style="display: inline-block;" /> <a href="#">{{this.Headers}}</a> </h3> <div class="tblDiv"> <table class="itemsContentTbl"> <tr> <th class="itemsContentHeader"></th> <th></th> <th>Pack Qty</th> <th>Pack Size</th> </tr> {{#each this.Contents}} <tr> <td class="itemDescription">{{this.ItemDescription}}</td> <td class="tdCkItemDesc"> <input type="checkbox" id="ck-{{this.ItemDescription}}" name="ck-{{this.ItemDescription}}" /> </td> <td class="packQty" style="text-align: center"> <input type="text" id="txtPackQuantity-oi-{{this.ItemDescription}} name="txtPackQuantity-oi-{{this.ItemDescription}} /> </td> <td class="packSize">{{this.PackSize}}</td> <input type="hidden" id="hdnPackSize-oi-{{this.ItemDescription}}-ps-{{this.PackSize}}" name="hdnPackSize-oi-{{this.ItemDescription}}-ps-{{this.PackSize}}" value="{{this.PackSize}}" /> </tr> {{/each}} </table> </div> </div> {{/each}} </div> </script>
Not all of the classes will be styled, some are just used as hooks for jQuery. However, note the span tag in the template. We need all of the headers to have the plus icon when the page loads. Since we are not using the accordion we need to leverage the jQuery UI css. The ui-icon and ui-icon-plus classes will allow us to achieve this. Also, in order for the icon to be displayed correctly, the span element must be rendered as an inline-block. It is quite common when using the accordion to see the icons smashed into the text, inline-block is the solution. And again, just inlining the css for illustrative purposes.
Let's take care of the AJAX call and the template so we can see what we are working with.
<script src="~/Scripts/jquery-1.8.2.js"></script> <script src="~/Scripts/jquery-ui-1.8.24.js"></script> <script src="~/Scripts/handlebars.js"></script> <script type="text/javascript"> $(function() { $.ajax({ url: '@Url.Action("GetItemData")', contentType: 'application/json; charset=utf-8', dataType: 'json', cache: false, success: function(data) { var source = $('#itemsTmpl').html(); var template = Handlebars.compile(source); $('#tmplTarget').html(template(data)); }, error: function(jqXHR, textStatus, errorThrown) { alert(errorThrown); } }); }); </script>
.itemsContentTbl { border-collapse: collapse; /* center table */ margin: 10px auto 10px auto } .itemsContentTbl th { background: #002a5f; color: white; padding-left: 20px; padding-right: 20px; border: 1px solid black; text-align: center; } .itemsContentTbl td { border: 1px solid black; text-align: center; vertical-align: middle; }Let's have a look....
In the Handlebars template, we just need to add classes to the checkbox and textbox inputs. Add the classes ckItemDesc for the checkbox and txtPackQty for the textbox. The css for those classes:
.ckItemDesc { border: none !important; padding-left: 30px; } .txtPackQty { width: auto; margin-left: 60px; margin-right: 30px; }
$('.itemsContentHeader, .itemDescription').hide();
$('h3').addClass('ui-accordion-header ui-helper-reset ui-state-default ui-corner-all');
$('.tblDiv').addClass('ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom');
$('.tblDiv').hide();
$('input:text[id^="txtPackQuantity"]').change(function () { $(this).parent().prev().children().attr('checked', 'checked'); });
Right now the panel control is not responding to mouse clicks. The next part is where all the heavy lifting comes in. The toggle function in jQuery is exactly what we need to make this work. The toggle function is passed in two functions, one for the first mouse click, and the second function for the second mouse click. As we have been doing, we will be harnessing jQuery UI css classes to make all the magic happen. Since the span element in the template has the icon, that span will be the target for the toggling.
$('span').toggle( function() { $(this).addClass('ui-icon-minus'); $(this).parent().next().removeClass('ui-accordion-header ui-helper-reset ui-state-default ui-corner-all').addClass('ui-accordion-header ui-helper-reset ui-state-default ui-state-active ui-corner-top'); $(this).parent().next().show('slow'); }, function() { var ckBox = $(this).parent().parent().find('.ckItemDesc'); if ($(ckBox).is(':checked')) { $(this).removeClass('ui-icon-plus'); $(this).addClass('ui-icon ui-icon-minus'); return; } else { $(this).removeClass('ui-icon-minus'); $(this).addClass('ui-icon-plus'); $(this).parent().next().removeClass('ui-accordion-header ui-helper-reset ui-state-default ui-state-active ui-corner-top').addClass('ui-accordion-header ui-helper-reset ui-state-default ui-corner-all'); $(this).parent().next().hide('slow'); } } );
The first time the plus icon is clicked, we add the minus icon. Then we remove the css classes that are used when a panel is not displayed and add the css classes for when a panel is displayed/active. Finally, since that div is hidden, we just show it.
On the second click, if the checkbox is checked, we need to freeze the panel. To do that, it's just simply a matter of removing the plus icon css class and adding the minus icon css class. Using the return keyword will break out of the code.
If the checkbox is not clicked, we need to remove the minus icon css class, add the plus icon css class, remove the css classes indicating the panel is active, add the css classes indicating the panel is not active, and finally, hide that div.
Now we should be able to have multiple panels open, freeze panels, and expand collapse panels where the text box does not have any data.
Below is all the jQuery code together.
<link href="~/Content/themes/base/jquery.ui.all.css" rel="stylesheet" /> <script src="~/Scripts/jquery-1.8.2.js"></script> <script src="~/Scripts/jquery-ui-1.8.24.js"></script> <script src="~/Scripts/handlebars.js"></script> <script type="text/javascript"> $(function() { $.ajax({ url: '@Url.Action("GetItemData")', contentType: 'application/json; charset=utf-8', dataType: 'json', cache: false, success: function(data) { var source = $('#itemsTmpl').html(); var template = Handlebars.compile(source); $('#tmplTarget').html(template(data)); // do not want to show content headers and descriptions $('.itemsContentHeader, .itemDescription').hide(); // style the headers $('h3').addClass('ui-accordion-header ui-helper-reset ui-state-default ui-corner-all'); // style divs below header $('.tblDiv').addClass('ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom'); // close the divs $('.tblDiv').hide(); // checking checkbox will be cue to freeze panel, check when field changes $('input:text[id^="txtPackQuantity"]').change(function() { $(this).parent().prev().children().attr('checked', 'checked'); }); // toggle plus minus $('span').toggle( function() { $(this).addClass('ui-icon-minus'); $(this).parent().next().removeClass('ui-accordion-header ui-helper-reset u-state-default ui-corner-all').addClass('ui-accordion-header ui-helper-reset ui-state-default ui-state-active ui-corner-top'); $(this).parent().next().show('slow'); }, function() { var ckBox = $(this).parent().parent().find('.ckItemDesc'); if ($(ckBox).is(':checked')) { $(this).removeClass('ui-icon-plus'); $(this).addClass('ui-icon ui-icon-minus'); return; } else { $(this).removeClass('ui-icon-minus'); $(this).addClass('ui-icon-plus'); $(this).parent().next().removeClass('ui-accordion-header ui-helper-reset ui-state-default ui-state-active ui-corner-top').addClass('ui-accordion-header ui-helper-reset ui-state-default ui-corner-all'); $(this).parent().next().hide('slow'); } } ); }, error: function(jqXHR, textStatus, errorThrown) { alert(errorThrown); } }); }); </script>