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

Sunday, September 29, 2013

Handlebars & jQuery UI Accordion (Sort Of) Part 2

We will be picking up from where we left off from the most recent post. Our friends in "the business" have come up with a new request since we went out for celebratory pizza. This is what they want now....

"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>

 
We'll come back to the jQuery code later. For now, let's take care of some styling.
.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....

 
Ok we have two issues we need to clean up. The checkbox has a border around it, which was introduced by the css class itemsContentTbl td. Also, we should center the textbox.
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;
}


 
Everything else will be done with jQuery. All of the following code should be placed in the AJAX success callback. We do not want to display the item descriptions in the html table so we have to hide those.
$('.itemsContentHeader, .itemDescription').hide();

 
Style the headers for our panel/accordion.
$('h3').addClass('ui-accordion-header ui-helper-reset ui-state-default ui-corner-all');

 
Style the divs below the header.
$('.tblDiv').addClass('ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom');

 
The divs that we just styled have to be hidden upon page load so the accordion/panel appears to be closed.
$('.tblDiv').hide();
 
Now we need to take care of the checkbox. When a user enters a value into the textbox, we would like that to automatically check the check box. If a checkbox is clicked, we will use that as an indication to keep that panel open, essentially freezing it. You may say, well, if a user manually checks the checkbox and does not enter a value into the textbox, it will still freeze the panel right? Correct. This may not be ideal but a user could check it as a reminder to come back and decide if they want to enter a value into the checkbox. The checkbox is optional, things will work just as well without it. Anyway, the code:
$('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>

Monday, September 23, 2013

Handlebars & jQuery UI Accordion Part 1

This post is modeled after something I have recently done at work. Here is the rough description....

The application is an ASP.NET Web Forms application that is at least five years old. The DevExpress tool suite is used heavily throughout the site. The business wanted to make a presentation change of a grid, thing is, the DevExpress grid could not do it. I was tasked with handling this so I went with Handlebars.js and the Accordion control from jQuery UI. It was necessary to use templating as the data was dynamic. This ended up being a very nasty task due to various button click handlers and the nasty postback behavior of ASP.NET.

For this walkthrough, I'm going with ASP.NET MVC just to simplify things. Make sure to add Handlebars through NuGet to the solution. Let's start with the models we will be using.....
public class Material
{
   public string MaterialDescription { get; set; }
   public int RequestQty { get; set; }
   public int PackQty { get; set; }
   public int PackSize { get; set; }
   public int TotalApproved { get; set; }
   public int AvailableInv { get; set; }
}

I'm adding a view model to send to the client because generally, I do not like returning anonymous objects. When I can, I always like to be explicit and declare the type. The view model:
public class IndexViewModel
{
   public string Headers { get; set; }
   public IEnumerable<Material> Contents { get; set; }
}

The Index method of whatever controller you are using can be left as is. Add the view. The view will contain the target div for the accordion as well as the Handlebars template. The view:
@model IEnumerable<YourSolution.Models.IndexViewModel>
@{
   ViewBag.Title = "Handlebars Accordion";
}


<div id="target"></div>

<script id="testTmpl" type="text/x-handlebars-template">
   <div id="test">
      {{#each this}}
         <h3><a href="#">{{this.Headers}}</a><h3>
         <div>
            <table class="table">
               <tr>
                  <th>Request Qty</th>
                  <th>Pack Qty</th>
                  <th>Pack Size</th>
                  <th>Total Approved</th>
                  <th>Available Inv</th>
               </tr>
               {{#each this.Contents}}
                  <tr>
                     <td>{{this.RequestQty}}</td>
                     <td>{{this.PackQty}}</td>
                     <td>{{this.PackSize}}</td>
                     <td>{{this.TotalApproved}}</td>
                     <td>{{this.AvailableIn}}</td>
                  </tr>
               {{/each}}
            </table>
         </div>
      {{/each}}
   </div>
</script>

Handlebars is pretty slick and easy to use. To loop through a collection is fairly straightforward, just use #each and this, where this of course refers to the current item. This also shows a nested data structure and how to perform nested looping. We have two properties in the view model, Headers and contents. In the first each, we are just writing out the Headers property. To access any properties you just use dot syntax. When you want to access the nested object, just embed another each, and you will have access to the nested property/object.
Let's move on to the JavaScript. For simplicity, I am putting it in the view as well.
<script type="text/javascript">
   $(function() {
      $.ajax({
         url: '/AccordionTmpl/GetData',
         type: 'GET',
         contentType: 'application/json; charset=utf-8',
         dataType: 'json',
         success: function(data) {
            var source = $('#testTmpl')html();
            var template = Handlebars.compile(source);
            $('#target').html(template(data));

            $('#test').accordion({
               active: false,
               header: 'h3',
               collapsible: true,
               icons: {
                  "header": "ui-icon-plus",
                  "headerSelected": "ui-icon-minus"
               }
            });
         },
         error: function(response) {
            alert(response.responseText);
         }
      });
   });
</script>

If you've done any templating this should look familiar. Grab the html of the template, compile it, feed the data into the template, take the html and add it to the target. The accordion configuration is pretty basic as well.
There is one css class in our template. We will add some styling to the table, headers and rows.
.tbl {
   border-collapse: collapse;
   /* center the table */
   margin: 10px auto 10px auto;
}

.tbl th {
   background: #002a5f;
   color: white;
   padding-left: 20px;
   padding-right: 20px;
   border: 1px solid black;
}

.tbl td {
   border: 1px solid black;
   text-align: center;
}
We have to go back to the server and write the method to return data:
[HttpGet]
public ActionResult GetData()
{
   List<Material> materials = new List<Material>()
   {
      new Material(){MaterialDescription = "Big Bang Theory Sheldon Shirt", RequestQty = 3, PackQty = 3, PackSize = 3, TotalApproved = 3, AvailableInv = 6},
      new Material(){MaterialDescription = "Big Bang Theory Leonard Shirt", RequestQty = 2, PackQty = 2, PackSize = 2, TotalApproved = 2, AvailableInv = 7},
      new Material(){MaterialDescription = "Big Bang Theory Penny Shirt", RequestQty = 1, PackQty = 1, PackSize = 1, TotalApproved = 1, AvailableInv = 9},
      new Material(){MaterialDescription = "Big Bang Theory Kripke Shirt", RequestQty = 1, PackQty = 1, PackSize = 1, TotalApproved = 1, AvailableInv = 10},
   };

   IEnumerable<IndexViewModel> data = materials
                                            .GroupBy(x => x.MaterialDescription)
                                            .Select(y => new IndexViewModel()
                                            {
                                               Headers = y.Key, 
                                               Contents = y.Select(z => z).ToList()
                                            });

   return Json(data, JsonRequestBehavior.AllowGet);
}

Let's run and see what we got...
 

Not too shabby. We did some good work, should probably reward ourselves with some pizza.

*One hour later*

It seems our friends in the business saw what we did and they have a request. Since the list of items can be quite a few, they would like it if more than one panel could stay open so a user can remember where they were.

This can be done even though the accordion was not designed to do this. The solution, initially, seems very simple. The answer lies in the structure of the accordion. You have to change the target of the accordion. If you look at the documentation for the accordion, the structure of your html has to look like this:

<div id="accordion">
   <h3>First Header</h3>
   <div>First Content Panel</div>
   <h3>Second Header</h3>
   <div>Second Content Panel</div>
</div>

Our structure is different but still worked. We are going to change the target for the accordion from $('#test) to $('#test > div'). Our target is now the child div of the test div. The only other change we need to make is to add some css to the anchor tag, right after the accordion declaration. Make the changes:
$('#test > div').accordion({
   active: false,
   header: 'h3',
   collapsible: true,
   icons: {
      "header": "ui-icon-plus",
      "headerSelected": "ui-icon-minus"
   }
});

$('a').css({ "display": "inline-block" });

Now to run...
 
Looks good! The css that was added to the anchor tag is very, very important. If you are ever using the accordion and your icons are mashed in with the text, applying that css to the anchor will solve it. Another solution is the following:
<h3>
   <span style="display: inline-block;" />
   <a href="#">{{this.Headers}}</a>
</h3>

Really the only difference is to what element you apply the css. Again, just inlining the css for illustrative purposes.

In the next post, we will revisit this scenario as our friends from the business have another request.

Sunday, June 30, 2013

jQuery: When Hide Doesn't Hide

I was recently working on a project and the requirement was basically this: A user can have up to five emergency contacts. This was a MVC project. My line of thinking was this....

-The main view model has a property List<EmergencyContactViewModel>.
-EmergencyContactViewModel contained props for name, relationship, home phone, work phone.
-When displaying this in edit mode, there would always be at least one emergency contact.

Given that, you know that you had 4 more contacts to add to the form. I thought, no problem, just add all the fields and then hide them. To my surprise, that did not work. That is the center of this posting.

Here is the abbreviated view:

<div>
   <ul>
      <li><a href="#tabs-1">My Information</a></li>
      <li><a href="#tabs-2">Emergency Contact</a></li>
      <li><a href="#tabs-3">Federal Tax</a></li>
      <li><a href="#tabs-4">State Tax</a></li>
      <li><a href="#tabs-5">Change Request</a></li>
   </ul>
   <div id="tabs-1">
   </div>
   <div id="tabs-2">
   </div>
   <div id="tabs-3">
   </div>
   <div id="tabs-4">
   </div>
   <div id="tabs-5">
      @using (Html.BeginForm())
      {
         <table id="changeRequest">
            <tr>
               <td>Emploee ID</td>
               <td>@Html.TextBoxFor(x => x.MyInfo.EmployeeId)</td>
               <td>Address</td>
               <td>@Html.TextBoxFor(x => x.MyInfo.Address&lt/td>
               <td></td>
               <td></td>
            </tr>
            <tr>
               <td>Last Name</td>
               <td>@Html.TextBoxFor(x => x.MyInfo.LastName)</td>
               <td></td>
               <td><input type="text"></td>
               <td></td>
               <td></td>
            </tr>
            <tr>
               <td>First</td>
               <td>@Html.TextBoxFor(x => x.MyInfo.FirstName)</td>
               <td></td>
               <td><input type="text"></td>
               <td></td>
               <td></td>
            </tr>
            <tr>
               <td>Middle</td>
               <td>@Html.TextBoxFor(x => x.MyInfo.MiddleName)</td>
               <td>City</td>
               <td>@Html.TextBoxFor(x => x.MyInfo.City)</td>
               <td></td>
               <td></td>
            </tr>
            <tr>
               <td>Email</td>
               <td>@Html.TextBoxFor(x => x.MyInfo.Email)</td>
               <td>State</td>
               <td>@Html.TextBoxFor(x => x.MyInfo.State)</td>
               <td></td>
               <td></td>
            </tr>
            <tr>
               <td></td>
               <td></td>
               <td>Zip Code</td>
               <td>@Html.TextBoxFor(x => x.MyInfo.ZipCode)</td>
               <td></td>
               <td></td>
            </tr>
            <tr>
               <td></td>
               <td></td>
               <td>Phone 1</td>
               <td>@Html.TextBoxFor(x => x.MyInfo.Phone1)</td>
               <td></td>
               <td></td>
            </tr>
            <tr>
               <td></td>
               <td></td>
               <td>Phone 2</td>
               <td>@Html.TextBoxFor(x => x.MyInfo.Phone2)</td>
               <td></td>
               <td></td>
            </tr>
            <tr>
               <td><input type="button" id="addContact" value="Add Contact" /></td>
            </tr>
            <tr>
               <td>Contact</td>
               <td>Relationship</td>
               <td>Home Phone</td>
               <td>Work Phone</td>
            </tr>
            @for (var i = 0; i < Model.EmergencyContacts.Count; i++)
            {
               <tr>
                  <td>@Html.TextBoxFor(x => x.EmergencyContacts[i].ContactName)</td>
                  <td>@Html.TextBoxFor(x => x.EmergencyContacts[i].Relationship)</td>
                  <td>@Html.TextBoxFor(x => x.EmergencyContacts[i].HomePhone)</td>
                  <td>@Html.TextBoxFor(x => x.EmergencyContacts[i].WorkPhone)</td>
               </tr>
            }
         </table>
         <input type="submit" value="Submit" />
      }
   </div>
</div>


A few things to point out...
Yes the for loop should be removed and be replaced with an Editor Template. However, this was the standard and how they did things so just keeping in line.

The easiest solution would be that since an employee can have up to five emergency contacts, just render all the fields right out of the gate. Afterall, this is in Edit mode, however, in this post we are going to be doing it dynamically.

Let's take a look at the form...


To do this dynamically, we will need to test for the existence of other rows since an employee can have up to five emergency contacts. If the row exists, of course, don't add. If the row doesn't exist, add a new row:

if ($('#EmergencyContacts_1__ContactName').length < 1) {
   $('#changeRequest > tbody:last').append('<tr id="contact1">' +
      <td><input name="EmergencyContacts[1].ContactName" id="EmergencyContacts_1__ContactName" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[1].Relationship" id="EmergencyContacts_1__Relationship" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[1].HomePhone" = id="EmergencyContacts_1__HomePhone" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[1].WorkPhone" = id="EmergencyContacts_1__WorkPhone" +
      type="text" /><td>
   );
}

if ($('#EmergencyContacts_2__ContactName').length < 1) {
   $('#changeRequest > tbody:last').append('<tr id="contact2">' +
      <td><input name="EmergencyContacts[2].ContactName" id="EmergencyContacts_2__ContactName" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[2].Relationship" id="EmergencyContacts_2__Relationship" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[2].HomePhone" = id="EmergencyContacts_2__HomePhone" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[2].WorkPhone" = id="EmergencyContacts_2__WorkPhone" +
      type="text" /><td>
   );
}

if ($('#EmergencyContacts_3__ContactName').length < 1) {
   $('#changeRequest > tbody:last').append('<tr id="contact3">' +
      <td><input name="EmergencyContacts[3].ContactName" id="EmergencyContacts_3__ContactName" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[3].Relationship" id="EmergencyContacts_3__Relationship" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[3].HomePhone" = id="EmergencyContacts_3__HomePhone" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[3].WorkPhone" = id="EmergencyContacts_3__WorkPhone" +
      type="text" /><td>
   );
}

if ($('#EmergencyContacts_4__ContactName').length < 1) {
   $('#changeRequest > tbody:last').append('<tr id="contact4"> +
      <td><input name="EmergencyContacts[4].ContactName" id="EmergencyContacts_4__ContactName" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[4].Relationship" id="EmergencyContacts_4__Relationship" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[4].HomePhone" = id="EmergencyContacts_4__HomePhone" +
      type="text" /><td> +
      <td>< input name="EmergencyContacts[4].WorkPhone" = id="EmergencyContacts_4__WorkPhone" +
      type="text" /><td>
   ');
}


Now it seems the next thing to do would be to add code to hide those table rows and handle the button click event as such:

$('#contact1').hide();
$('#contact2').hide();
$('#contact3').hide();
$('#contact4').hide();

$('#addContact').click(function() {
   $('#contact1').show();
   $('#contact2').show();
   $('#contact3').show();
   $('#contact4').show();
});


Let's go take a look at the result...



The rows are not hidden. The solution is rather simple. All that is needed is to add the visibility attribute with a value of hidden to each table row. For example:

$('#changeRequest > tbody:last').append('<tr id="contact1" style="visibility: hidden;">'


The next step is to handle the click event for the Add Contact button. For each row, change the visibility attribute value to visible, then hide it, then fade in. It may seem odd to show the table row and then hide it again. This is done so we can apply the fade in effect. If you omit hiding the second time, all the fields will just pop in immediately. Of course that is fine, we just want to show a smooth transition:

$('#addContact').click(function () {
   $('#contact1').css('visibility', 'visible').hide().fadeIn('slow');
   $('#contact2').css('visibility', 'visible').hide().fadeIn('slow');
   $('#contact3').css('visibility', 'visible').hide().fadeIn('slow');
   $('#contact4').css('visibility', 'visible').hide().fadeIn('slow');
});


When the page loads, the fields should be hidden...


Then when Add Contact is clicked, the fields should fade in nicely...

Monday, June 3, 2013

jQuery Modal Dialog & Scrolling

Recently I was loading some documents into a jQuery modal dialog. Everything was going well until I added content to the dialog and all of a sudden, when running the page and opening the dialog, it would scroll to the bottom of the document. Not sure what causes this but someone suggested it may be caused due to a link.

Now to fix that annoying behavior...

The element clicked to cause the dialog to open:
<div id=tabs">
   <a href="#" class="modal">The Modal</a>
</div>
We have a div that contains the contents of the modal:
<div id="modalContent">
   // content here
</div>

At the top of that div, sneak in another div that contains a link, this is going to be the target that is set in the modal dialog:
<div id="modalContent">
   <div id="topPosition">
      <a href="#" class="ui-dialog-relative">&nbsp;</a>
   </div>
   // other modal content
</div>

The jQuery:
$('a.modal').click(function (e) {
   e.preventDefault();
   $('#modalContent').dialog({
      modal: true,
      autoOpen: false,
      width: 1100,
      buttons: {
         Print: function () {
            window.print();
         },
         Close: function () {
            $(this).dialog("close");
         }
      },
      open: function () {
         $('.ui-dialog-relative :link').blur();
         $('#topPosition').focus();
      },
      focus: function (event, ui) {
         $('#topPosition').focus();
      }
   });

   $('#modalContent').dialog('open');
});

The key is to hook into the ui-dialog-relative href and give that the focus when the dialog opens. Calling blur ensures that the link itself is not visible.

This did solve the initial problem, however going back to this a few days later I removed the open and focus functions from the dialog settings and things worked. Strange but initially it did solve the problem.

Sunday, May 5, 2013

ASP.NET MVC, jQuery, Checkboxes, Tables, JSON

In this post we will be looking at the following:
1). Creating and populating a HTML table.
2). Clicking a button in the table, causing it to check a checkbox.
3). Clicking button outside of table, triggering a modal dialog to be loaded.
4). Send data back to the controller based on which checkbox was checked.
5). Send the data as text and then send it as JSON.

Here is what we are building, the main screen:


When a user clicks the Notify button off to the right, the checkbox in that row should be checked. Note in the screenshot Leonard and Raj are selected.


The user will click the Send Notifications button after they have made all of their selections. This will trigger a modal dialog to appear on the screen. The dialog will be populated with the email address of the selected/checked row in the table. The user can then fill out the body and reason, then have the email sent.


Spin up a new MVC project. Add a PersonViewModel class to the models folder:
public class PersonViewModel
{
   public int PersonId { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public string Age { get; set; }
   public string FavoriteFood { get; set; }
   public string Email { get; set; }
}

In the HomeController, delete the view. We will create a method to give us some data to play with. We will come back to the view in a moment.
public ActionResult Index()
{
   List<PersonViewModel> people = GetPeople();
   return View(people);
}

private static List<PersonViewModel> GetPeple()
{
   return new List<PersonViewModel>()
   {
      new PersonViewModel() { PersonId = 1, FirstName = "Sheldon", LastName = "Cooper", Age = "35", FavoriteFood = "Greek Food", Email = "sheldon@bigbang.com" }, 
      new PersonViewModel() { PersonId = 2, FirstName = "Leonard", LastName = "Hoffstadter", Age = "35", FavoriteFood = "Cheese", Email = "leonard@bigbang.com" },
      new PersonViewModel() { PersonId = 3, FirstName = "Raj", LastName = "Koothrappali", Age = "35", FavoriteFood = "Indian Food", Email = "raj@bigbang.com" },
      new PersonViewModel() { PersonId = 4, FirstName = "Howard", LastName = "Wallowitz", Age = "35", FavoriteFood = "Brisket", Email = "howard@bigbang.com" },
      new PersonViewModel() { PersonId = 5, FirstName = "Stuart", LastName = "Bloom", Age = "35", FavoriteFood = "Can of tuna", Email = "stuart@bigbang.com" }
   };
}

Now let's set up the view. The view will be strongly typed to an IEnumerable of PersonViewModel. Be sure to add script references for jQuery and jQuery UI, either inline or through bundling:
@model IEnumerable<YourProjectName.Models.PersonViewModel>

@{
   ViewBag.Title = "Index";
}

<div id="notificationDiv">
   <input type="button" id="btnSendNotifications" name="btnSendNotifications" value="Send Notifications" class="ui-button ui-button-text-only ui-widget ui-state-default ui-corner-all" />
</div>

<table id="tblPeople">
   <tr class="tableHeader">
      <th></th>
      <th>Id</th>
      <th>First Name</th>
      <th>Last Name</th>
      <th>Age</th>
      <th>Favorite Food</th>
      <th>Email</th>
      <th></th>
   </tr>

   @foreach (var item in Model)
   {
      <tr>
         <td>
            <input type="checkbox" name="ckCheck" id='@("ckCheck"+@item.PersonId)' />
         </td>
         <td id="personId">@item.PersonId</td>
         <td id="firstName">@item.FirstName</td>
         <td id="lastName">@item.LastName</td>
         <td id="age">@item.Age</td>
         <td id="favoriteFood>@item.FavoriteFood</td>
         <td id="email">@item.Email</td>
         <td>
            <input type="button" id='@("btnNotify"+item.PersonId)' name="btnNotify" value="Notify" class="ui-button ui-button-text-only ui-widget ui-state-default ui-corner-all" />
         </td>
      </tr>
   }
<table>

Let's break this down a bit. The notificationDiv at the top of the page is what will take care of sending the notifications. As we will see, the click event will spin through the table rows to find which checkboxes are clicked and grab the email addresses. We are using jQuery UI to style the buttons and make them a little more pretty. Next we go through and create the table header. Then we spin through the model and write out the data.

A few things to note. I am being a little lazy and using foreach here. MVC purists would say to use a partial view as to get rid of the logic in the view. I agree but I admit I am being a little lazy. Note the technique for getting unique ids for the checkboxes and the Notify button. Razor syntax makes this possible. Here we are using it in two places: id='@("ckCheck"+@item.PersonId)' and id='@("btnNotify"+item.PersonId)'. I like this technique as it is an easy way to get unique ids.

On to the next order of business. We are going to set up a modal dialog. The purpose of this dialog is to display to the user all the email addresses to be sent back to the server. This will give a nice visual display of letting them double check their selections. They can then enter the message and reason, and finally, submit the request. Right after the table declaration in the view, add:

<div id="dialogTarget" title="Send Notifications>
   <p>All form fields are required.</p>
   <table id="peeps">
      <tr>
         <td>Bcc Message To:</td>
         <td>@Html.TextBox("ccTo", "", new { style = "width:300px;" })</td>
      </tr>
      <tr>
         <td>Message:</td>
         <td>@Html.TextArea("message", new { style = "width:300px;height:120px;" })</td>
      </tr>
      <tr>
         <td>Reason:</td>
         <td>@Html.TextBox("Reason", "", new { style = "width:300px;" })</td>
      </tr>
   </table>
</div>

All of the HTML is now good to go. Let's lay in all the script. Again, make sure your script references are set up the way you like them. We will go through piece by piece. First we will add some CSS to our table rows for the zebra striping effect. Will come back later and fill in the css business:

$('#tblPeople tr:even').addClass("alt");
$('#tblPeople tr:odd').addClass("altOdd");

Each row in the table has a Notify button. We need to handle the click event for this button and find the checkbox in that row, then check that checkbox. Since our buttons and checkboxes have unique ids, we will use the input selector 'input[name^="btnNotify"]'. The name^ syntax means "attribute starts with". We will grab the last character of the button id which will be a number. That number will then tell us which checkbox we should find, then check that checkbox:
$('input[name^="btnNotify"]').click(function (e) {
   var lastChar = e.target.id.substr(e.target.id.length - 1);
   ($(e).closest('td').find($('#ckCheck' + lastChar).attr('checked', true)));
});
The Send Notifications button click event has to go through each row in the table and find which checkboxes have been clicked. When the checkbox state is checked, we have to grab the email address from that row and put it into an array. This click event will also set the value of the ccTo field of the dialog. The last step of the event is to open the dialog.
$('#btnSendNotifications').click(function (e) {
   var emailAddresses = new Array();

   $('#tblPeople tr').each(function () {
      var checkbox = $(this).find($('input[name="ckCheck"]'));

      if (checkbox.is(":checked")) {
         var email = $(this).find('#email');
         emailAddresses.push(email.text());
      }
   });

   $('#ccTo').val(emailAddresses);
   $('#dialogTarget').dialog('open');
});

When the modal dialog pops open it should display the email addresses that were selected. The user can then fill out the message and the reason. Clicking the Notify button will fire off an Ajax request back to the server. When we return from the server we have to make sure that all selected checkboxes are changed to not being selected:
$('#dialogTarget').dialog({
   modal: true,
   autoOpen: false,
   width: 500,
   buttons: {
      "Notify": function (e) {
         var ccTo = $('#ccTo').val();
         var message = $('#message').val();
         var reason = $('#reason').val();
         $.ajax({
            url: '@Url.Action("EmailInfo", "Home")',
            type: "POST",
            dataType: "text",
            data: ({ ccTo: ccTo, message: message, reason: reason }),
            success: function (result) {
               alert(result);
               $('#dialogTarget').dialog('close');
               $('#tblPeople tr').each(function () {
                  var checkBox = $(this).find($('input[name="ckCheck"]'));

                  if (checkBox.is(":checked")) {
                     checkBox.attr('checked', false);
                  }
               }
            },
            error: function (jqXHR) {
               alert('Error: ' + jqXHR.responseText);
            }
         });
      },
      "Cancel": function () {
         $(this).dialog('close');
         $('#tblPeople tr').each(function () {
            var checkBox = $(this).find($('input[name="ckCheck"]'));

            if (checkBox.is(":checked")) {
               checkBox.attr('checked', false);
            }
         });
      }
   }
});

I can already hear the cringing....what the, why are you sending text data back?!?!? Hey now, just showing something different. We will come back and make it JSON. Let's take care of the server side business. We will need an Action called EmailInfo that will be expecting three string parameters. This Action will simple return a content message.
[HttpPost]
public ContentResult EmailInfo(string ccTo, string message, string reason)
{
   string result = "Email has been sent.";
   return Content(result);
}

Ok let's switch it all over to JSON. We will need a view model, EmailViewModel:
public class EmailViewModel
{
   public string EmailAddresses { get; set; }
   public string Message { get; set; }
   public string Reason { get; set; }
}

Change the EmailInfo Action:
[HttpPost]
public ActionResult EmailInfo(EmailViewModel email)
{
   if (ModelState.IsValid)
   {
      return Json(new { Message = "Email has been sent." });
   }
   else
   {
      return Json(new { Message = "Error, email was not sent." });
   }
}

The only change that we need to make in the JavaScript is in the Ajax call, more specifically, the data variable, and the success callback:
$.ajax({
   url: '@Url.Action("EmailInfo", "Home"),
   type: "POST",
   dataType: "json",
   data: {
      emailAddresses: ccTo,
      message: message,
      reason: reason
   },
   success: function (result) {
      alert(result.Message);
      $('#dialogTarget').dialog('close');
      $('#tblPeople tr').each(function () {
         var checkBox = $(this).find($('input[name="ckCheck"]'));

         if (checkBox.is(":checked")) {
            checkBox.attr('checked', false);
         }
      });
   },
   error: function (jqXHR) {
      alert('Error: ' + jqXHR.responseText);
   }
});

The final step is the css garbage. First, we will remove the border that is put around the checkboxes:
input[type="checkbox"] {
   background: transparent;
   border: none;
   width: auto;
}

And the css for the table:
table {
   border: solid 1px #e8eef4;
   border-collapse: collapse;
}

table td {
   padding: 5px;
   border: solid 1px #e8eef4;
   border-color: black;
}

table th {
   padding: 6px 5px;
   text-align: left;
   border: solid 1px #e8eef4;
}

tr.alt td {
   background-color: #F7F7DE;
   border-style: solid;
   border-width: 1px;
   border-color: black;
}

tr.altOdd td {
   border-style: solid;
   border-width: 1px;
   border-color: #000000
}

tr.tableHeader th {
   color: #FFFFFF
   background-color: #6B696B;
   font-weight: bold;
   border-style: solid;
   border-width: 1px;
   border-color: #000000;

Saturday, February 2, 2013

ASP.NET MVC, JSONP, jQuery UI

For this post, we're going to dive into JSONP with MVC. The aim is to return content from another site and display it in a modal dialog. We will need two separate web projects running at the same time to see this in action. Environment is VS2012, ASP.NET MVC 4.

We'll start with the client site. Just spin up a new MVC 4 project, call it Client. Open up the Index view for the HomeController and wipe everything out. This, will be the view:

<input id="txtBox1" type="text" />
<button id="getData">Get Data</button>
<div id="modal"></div>

@section scripts {

}

Add a new JavaScript files to the Scripts folder, name it Index.js. We'll just be setting up the modal dialog and the AJAX call:
$(document).ready(function() {
   $('#modal').dialog({
      title: "Bazinga",
      modal: true,
      autoOpen: false,
      height: 300,
      width: 500
   });

   $('#getData').click(function(e) {
      $.ajax({
         type: 'GET',
         dataType: 'jsonp',
         url: 'fill this out later,
         success: function(data) {
            $('#modal').html(data.data);
            $('#modal').dialog("open");
         },
         error: function(jqXHR, textStatus, errorThrown) {
            alert(textStatus);
            alert(errorThrown);
         }
      });
   });
});

Note the dataType on the AJAX call, jsonp. We will come back and fill out the URL once we set up the other site. If you want, you can use the AJAX $.getJSON method instead, as such:
$.getJSON('http://localhost:12345/Home/Index2?jsoncallback=?', function(data) {
   $('#modal').html(data.data);
   $('#modal').dialog("open");
});

To wrap up the Client site, open up BundleConfig in the App_Start folder. We will create a bundle for the scripts we need. Put this right at the top of the RegisterBundles method:
public static void RegisterBundles(BundleCollection bundles)
{
   bundles.Add(new ScriptBundle("~/bundles/app").Include(
               "~/Scripts/jquery-{version}.js",
               "~/Scripts/jquery-ui-{version}.js",
               "~/Scripts/Index.js"));
}

Jump over to _Layout.cshtml and just change the jQuery bundle to app...
@Scripts.Render("~/bundles/app")
Time to fire up another web project, call this one Widget. To make this work, we will need a JsonpResult. Also, we will be rendering our content from a view as a string. This is a pretty slick technique, and we have Rick Strahl to thank. Take some time and go check out his post to get all the details on it: Rendering ASP.NET MVC Views to String

First, let's take care of the JsonpResult. I added a folder called Extensions, added a new C# file called JsonpResult:
using System;
using System.Web;
using System.Web.Mvc;

public class JsonpResult : JsonResult
{
   public override void ExecuteResult(ControllerContext context)
   {
      if (context == null)
      {
         throw new ArgumentNullException();
      }

      HttpRequestBase request = context.HttpContext.Request;
      HttpResponseBase response = context.HttpContext.Response;
      string jsoncallback = (context.RouteData.Values["jsoncallback"] as string) ?? request["jsoncallback"];

      if (!String.IsNullOrEmpty(jsoncallback))
      {
         if (String.IsNullOrEmpty(base.ContentType))
         {
            base.ContentType = "application/x-javascript";
         }

         response.Write(String.Format("{0}(", jsoncallback));
      }

      base.ExecuteResult(context);

      if (!String.IsNullOrEmpty(jsoncallback))
      {
         response.Write(")");
      }
   }
}

The important part here is the jsoncallback string. We look in Values of RouteData for a jsoncallback string, if we find one, we return, if not, we look in the request. Then we write the content type out. You can see this in Fiddler, let's have a looky look....
 
Add another C# file to the Extensions folder, called ViewRenderer:
using System;
using System.Web.Mvc;

public class ViewRenderer
{
   protected ControllerContext Context { get; set; }

   public ViewRenderer(ControllerContext controllerContext)
   {
      Context = controllerContext;
   }

   public static string RenderPartialView(string viewPath, ControllerContext controllerContext)
   {
      ViewRenderer renderer = new ViewRenderer(controllerContext);
      return renderer.RenderPartialView(viewPath);
   }

   public string RenderPartialView(string viewPath)
   {
      return RenderViewToStringInternal(viewPath, true);
   }

   protected string RenderViewToStringInternal(string viewPath, bool partial = false)
   {
      ViewEngineResult viewEngineResult = null;

      if (partial)
      {
         viewEngineResult = ViewEngines.Engines.FindPartialView(Context, viewPath);
      }
      else
      {
         viewEngineResult = ViewEngines.Engines.FindView(Context, viewPath, null);
      }

      if (viewEngineResult == null)
      {
         throw new FileNotFoundException("The view could not be found");
      }

      IView view = viewEngineResult.View;
      string result = null;

      using (StringWriter sw = new StringWriter())
      {
         ViewContext ctx = new ViewContext(Context, view, Context.Controller.ViewData, Context.Controller.TempData, sw);
         view.Render(ctx, sw);
         result = sw.ToString();
      }

      return result;
   }
}

Now to set up the content that will be returned to the client. In the Views folder, add a folder named Tmpl. In the Tmpl folder, add a view named Widget.cshtml. This is just going to be a simple example of sending down some text and JavaScript. Of course you have to be careful, scripting attacks, etc. Anywho, the Widget.cshtml file:
<p>Remote HTML loaded via JSONP.</p>
<br /><br />
<input type="button" value="Press Me!" onclick="Message()" />

<script type="text/javascript">
   function Message() {
      alert("Bazinga punk!");
   }
</script>

The final step is setting up an ActionResult in the HomeController. All we need to do is pass the path of our widget and the controller context to the ViewRenderer, which will return a string. We use this string as the data for our JsonpResult. The GetWidget ActionResult method in the HomeController:
using Widget.Extensions;

[HttpGet]
public ActionResult GetWidget()
{
   string result = ViewRenderer.RenderPartialView("~/View/Tmpl/Widget.cshtml", ControllerContext);
   return new JsonpResult()
              {
                 Data = new { data = result },
                 JsonRequestBehavior = JsonRequestBehavior.AllowGet
              };
}

I almost forgot. Run the Widget project. We need to grab the URL and put it in the AJAX call in the Client project. All you will need to change is the port number for the localhost. The completed AJAX call should look as such:
$('#getData').click(function(e) {
   $.ajax({
      type: 'GET',
      dataType: 'jsonp',
      url: 'http://localhost:61691/Home/GetWidget?jsoncallback=?',
      success: function(data) {
         $('#modal').html(data.data);
         $('#modal').dialog("open");
      },
      error: function(jqXHR, textStatus, errorThrown) {
         alert(textStatus);
         alert(errorThrown);
      }
   });
});

You might be wondering, what the heck is up with that URL? Yes, yes. In order for cross-domain to work, you are required to add the "?jsoncallback=?" bit, that is the key. If you omit that, the call will not work.
Now to test it out. Fire up the Widget project, it will just appear as the default view since we didn't change any of that. Then fire up the Client project upon running....

 
Before we click the Get Data button, let's set a breakpoint in the Widget project. If everything works, we will hit it.

 
Click Get Data and...
 
Success! After stepping through the rest of that method, we should see the modal dialog pop up on the client....
 
The finale...