JQuery validation hooked up to HTML5 data attributes and some Bootstrap for a Salesforce page

by MikeHogg 9. October 2014 17:01

I was brought in as a front end developer on a Salesforce site to figure out a way to allow SF devs to add client side jQuery validation to dozens of fields as quickly and painlessly as possible.  Their application had only a few pages, but dozens of fields on each, and most of them required.  In addition, they had invisible widgets that were displayed and also required, based on answers of other fields.  So a page might be loaded with 10 fields, but when you check field number two, another panel with 3 fields shows up, and they are all required.

 

jQuery validation library is really easy to work with.  There are 7 or 8 standard validations that it knows already, like valid email addresses and required fields.  I just had to let the devs hook up with them easily, so I set up a scheme using new html5 data attributes.  I told them they just had to add

data-email=’must be a valid email address’

or

“data-required=’don’t forget!!’

to their html elements, and that was all.  I also gave them an easy way to have dependent validation, like when one field is required only if another field is checked.  And even another one for depending on a particular value being selected in dropdownlists, or any value being selected, that meant another field became required.

I came up with a quick script that hooked these data attributes up to the jQuery validator.  And used bootstrap style error messages.

var $jq = jQuery.noConflict();
 
/*
* customValidation.js
* Usage:
*      STANDARD BOOL VALIDATORS 
 *          add attribute data-{standard validation}="validation message"
*          to each html element that you want {standard validation like REQUIRED or EMAIL that are booleans}, including ones that show conditionally (like when you rerender a panel when clicking a radio)
*          {standard validation} is in jquery docs here http://jqueryvalidation.org/documentation/
*              includes required, email, url, date, dateISO, number, digits, creditcard
*          NOTE: if you are adding fields dynamically, from radio or checkbox selections, then their validations will be picked up automatically, on
*                  click, but if you are adding them with asynchronous ajax calls like with apex rerender actionsupport, 
 *                  then you need to add oncomplete="addValidation(this)" to your actionsupport tag
* 
 *      DEPENDS 
 *         add attribute  data-depends="htmlRenderedIdOfRadioOrCheckbox" data-val-message="validation message"
*         to each element that you want REQUIRED, based on whether RadioOrCheckbox is checked.  
 *         
 *      DEPENDS-DROPDOWN
*          same as DEPENDS, but also uses data-parm="{DropdownValue}" to test against.  
 * 
 *      DEPENDS-DROPDOWN-ANY
*          same as depends-dropdown, but no data-parm b/c it depends on anything being selected
*/
$jq().ready(function () {
    
    setValidation();
 
    $jq('input[type=radio],input[type=checkbox]').on('change', function () { addValidation(this) });
    
});
 
function setValidation(){
    var myRules = {}, myMessages = {};
     
    function extendValidator(validatorType) {
        var valType = validatorType.toString();
        var att = "[data-" + valType + "]";
 
        $jq(att).each(function (i) {
            var myBool = {};
            myBool[valType] = true;
            var myRule = {};
            myRule[this.id] = myBool;
            $jq.extend(true, myRules, myRule);
 
            var myText = {};
            myText[valType] = $jq(this).data(valType);
           var myMessage = {};
            myMessage[this.id] = myText;
            $jq.extend(true, myMessages, myMessage);
        });
 
    }
 
    $jq.each(["required", "email", "url", "date", "dateISO", "number", "digits", "creditcard"], function () { extendValidator(this); } );
 
    $jq("[data-depends]").each(function (i) {
        myRules[this.id] = {
            required: {
                depends: function (ele) {
                    return document.getElementById($jq(this).data("depends")).checked;
                }
            }
        };
        myMessages[this.id] = {
            required: $jq(this).data("val-message")
        };
    });
 
    $jq("[data-depends-dropdown]").each(function (i) {
        myRules[this.id] = {
            required: {
                depends: function (ele) {
                    return $jq(document.getElementById($jq(this).data("depends-dropdown"))).val() == $jq(this).data("parm");
                }
            }
        };
        myMessages[this.id] = {
            required: $jq(this).data("val-message")
        };
    });
 
    $jq("[data-depends-dropdown-any]").each(function (i) {
        myRules[this.id] = {
            required: {
                depends: function (ele) {
                    return $jq(document.getElementById($jq(this).data("depends-dropdown-any"))).prop('selectedIndex') > 0;
                }
            }
        };
        myMessages[this.id] = {
            required: $jq(this).data("val-message")
        };
    });
 
 
 
    // validate() initializer
    $jq("form").validate({
                                                                        debug:true,
        rules: myRules,
        messages: myMessages,
 
        // for accordion
        ignore: [],  
        invalidHandler: function (event, validator, element) { 
            if (validator.numberOfInvalids() > 0) {
                validator.showErrors();
                $jq(".has-error").closest(".panel-collapse:not(.in)").collapse('show');
                $jq(".has-error").closest(".panel-collapse:not(.in)").on('shown.bs.collapse', function () {
                    var arbitraryElement = $jq(".has-error").find("input")[0];
                    arbitraryElement.focus();
                    arbitraryElement.scrollIntoView(true);
                });
            }
        },
 
 
        // bootstrap 3.0 styles- you need to use bootstrap classes like class=form-group in your divs 
        errorElement: "span",
        errorClass: "help-block",
        highlight: function(element) {
            $jq(element).closest('.form-group').removeClass('has-success').addClass('has-error');
        },
        unhighlight: function(element) {
            $jq(element).closest('.form-group').removeClass('has-error').addClass('has-success');
            $jq(element).popover("hide");  // should be called in showErrors but when removing rules via Depends, showErrors doesn't fire
        },
        errorPlacement: function (error, element) {
            if (element.parent('.input-group').length || element.prop('type') === 'checkbox' || element.prop('type') === 'radio') {
                error.insertAfter(element.parent());
            } else {
                error.insertAfter(element);
            }
        },
 
        // bootstrap popovers, needs boostrap.js might conflict with jquery.ui.js
        showErrors: function (errorMap, errorList) {
 
            $jq.each(this.successList, function (index, value) {
                $jq(value).popover('hide');
            });
 
            $jq.each(errorList, function (index, value) {
                var _popover;
                _popover = $jq(value.element).popover({
                    trigger: "manual",
                    placement: "top",
                    content: value.message,
                    template: "<div class=\"popover\"><div class=\"arrow\"></div><div class=\"popover-inner\"><div class=\"popover-content\" style=\"font-weight:normal\"><p></p></div></div></div>"
                });
                _popover.data("bs.popover").options.content = value.message;
                $jq(value.element).popover("show");
            });
 
            this.defaultShowErrors();
        }
 
    });
}
 
/* 
 * used for creating rules after Validate()
*/
function addValidation(ele) {  
 
    $jq.each(["required", "email", "url", "date", "dateISO", "number", "digits", "creditcards"], 
        function(){
            var attType = this.toString();
            var attSelector = "[data-" + attType + "]";
            $jq(attSelector).each(function() {
                if (attType in $jq(this).rules() === false){
                    var myRule = {};
                    myRule[attType] = true;
 
                    var myMessage = {};
                    var msg = this.getAttribute("data-" + attType);
                    myMessage[attType] = msg;
                    myRule.messages = myMessage;
                    $jq(this).rules("add", myRule);
                }
            });
    });
     
}
 
 
function addMike(e) {
   if ($jq("#mike").length == 0) {  
        $jq("#lblFLName").append("<input type='text' id='mike' name='mike' value='' data-required='yesyesyes' placeholder='someplaceholder mike' ></input>");
    }
    else $jq("#mike").remove();
}

Tags:

JQuery

More js plugin fun, this time with with google maps

by MikeHogg 6. September 2012 09:40

I don't believe I've written this up anywhere and it might be useful to refer to in the future...  I did a rather extensive page in vb aspnet webforms that included mapping several markers on a google map.  Several lessons learned throughout.  Here is the mature code...

 

I was told to use a ListView control, and they wanted paging, so I hooked up the DataPager control to that, but they wanted the map to show everything in the list, not just the displayed page, so I just included a separate hidden listview, with my required data elements (all of them), no paging.  The data is a list of cars, each with a zip code as the sole location data.  I was to use this limited info to create markers for each car on the map.   Oh and the data was in a foreign language  :)  Everything else about the List should be standard here...

 

Added to the bottom of the html in this case...

<script language="javascript" type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=AIsomething&sensor=false"></script>
 <!-- deploy this script AFTER the maps api-->
 <script language="javascript" src="../_scripts/google-maps-3-vs-1-0.js" type="text/javascript"></script>
 <script language="javascript" type="text/javascript">
  var map; 
  var cars;
  var infowindow;
  var rollovers = [];
  var latlngprocessed = [];
  
  function init() {
   var mapOptions = {
    center: new google.maps.LatLng(51.55, 4.28), // default center
    zoom: 7,
    mapTypeId: google.maps.MapTypeId.ROADMAP
   };
               
   map = new google.maps.Map($("#CVMap")[0], mapOptions); 
 
   cars = getAllCars();
 
   $(cars).each(function () {
       if (!latLngAlreadyProcessed(this)) {                
           //  it's likely there exists more than one car at a particular latlng           
           var allmatches = getAllMatches(this);
 
           var image = new google.maps.MarkerImage('../_css/img/org_dwn_arrow.png',
                      new google.maps.Size(17, 19),
                      new google.maps.Point(0, 0),
                      new google.maps.Point(0, 19));
           var shadow = new google.maps.MarkerImage('../_css/img/org_dwn_arrow.png',
                      new google.maps.Size(40, 19),
                      new google.maps.Point(0, 0),
                      new google.maps.Point(0, 19));
           var shape = { coord: [1, 1, 1, 17, 19, 17, 19, 1],  type: 'poly' };
 
           var marker = new google.maps.Marker({
               map: map,
               position: getGoogleLatLng(this),
               shadow: shadow,
               icon: image,
               shape: shape,
               title: allmatches.length > 1 ? (allmatches.length) + ' cars' : this.titlelink.text
           });
 
           centerMap(allmatches);
           createInfoWindow();
 
           var $rollovercontent = $('<div class="carrollover" id="CarRollover"><h1></h1><ul></ul></div>');
           $.each(allmatches, function (idx, val) {
               var $item = $('<li></li>').html($(val.titlelink).clone())
        , $title = $('h1', $rollovercontent);
               if ($title.text() == '') {
                   $title.text('Cars in ' + val.location);
               }
               $('ul', $rollovercontent).append($item);
           });
 
           // closure and a separate array of rollovercontent needed here, because there is only one infowindow per map
           var i = rollovers.length; // get before push so we have index for closure below
           rollovers.push(getStringFromJqueryObject($rollovercontent));
 
           google.maps.event.addListener(marker, 'mouseover', (function (mark, idx) {
               return function () {
                   infowindow.setContent(rollovers[idx]);
                   infowindow.open(map, mark);
                   $('#CarRollover').parent().css('overflow-x', 'hidden');
                   setTimeout(function () {
                       $fix = $('#CarRollover').parent().parent();
                       $fix.css({'top' : '28px'});
                   }, 200);
               };
           })(marker, i));
       }
   });
  }

 

The closure is nothing more than creating distinct function instances on the fly for each rollover, since google maps only have one InfoWindow, we need to replace the content of it with that particular marker's info.  Also we merge info so a marker with several cars sharing the same zip code would show as a list of links.

The rest is just some standard helper jQuery-fu functions.

 

function centerMap(allmatches) { // to first in resultslist
      if ($.grep(allmatches, function (v) { return v.index == 0; }).length > 0) {
          map.setCenter(getGoogleLatLng(allmatches[0]));
      }
  }
 
  function createInfoWindow() {  // if not already created (google says only one per map)
      if (!infowindow) {
          infowindow = new google.maps.InfoWindow({
              maxWidth: 400
          });
      }
  }
  function getAllCars() {  
      var titlelinks = $("#hiddenformap .nameformap");
      var descriptions = $("#hiddenformap .shortdescriptionformap").map(function () { return $(this).text(); }).get();
      var locations = $(".hiddenlocationformap").map(function () { return $(this).text(); }).get();
      var latlngs = $(".hiddenlatlngformap").map(function () { return $(this).text(); }).get(); 
      var result = [];
      $.each(latlngs, function (idx, val) {
          if (val != '') {
              result.push({  
                        'index': idx,
                  'location': locations[idx],
                  'latlng': val,
                  'titlelink': titlelinks[idx],
                  'description': descriptions[idx]   });
          }
      });
   return result;
  }
 
  function getAllMatches(car) {
      return $.merge($.grep(cars, function (c) { return car.latlng == c.latlng && c != car; }), [car]);
  }
 
  function getGoogleLatLng(car) {
      return new google.maps.LatLng(car.latlng.split(",")[0], car.latlng.split(",")[1]);
  }
 
  function getStringFromJqueryObject(obj) {
      return $('<div>').append($(obj).clone()).html();
  }
 
        function latLngAlreadyProcessed(car) {
            var result = $.grep(latlngprocessed, function (ll) {
                return ll == car.latlng;
            }).length > 0;
            if (result == false) { latlngprocessed.push(car.latlng); }
            return result;
        }
One other thing to mention was that I was querying google for latitude and longitude and storing it server side, so as not to pound their geolocation service, as they requested in terms of service and by applying a few different limits.  So rather than sending zipcode (if you read the code above notice it should not send zip) it sends latlng which it already has.  Here is the server side code for google's geo service (note my XML library calls, which I now replace with 3.5 xml literals [yay] since I just found out about them)...

 

 

Private Function GetGoogleLatLng(ByVal c As Car, ByVal trynumber As Int16) As String
        Dim url As String = String.Format("http://maps.googleapis.com/maps/api/geocode/xml?sensor=false&address={0}", HttpUtility.HtmlEncode(j.FunctionLocationPostalCode))
        Dim x As XDocument = XDocument.Load(url)
        Dim s As XElement = x.Descendants("status")(0)
        If s Is Nothing Then
            'Logging.Log.
            Return String.Empty
        End If
 
        If s.Value = "OVER_QUERY_LIMIT" And trynumber < 4 Then
            System.Threading.Thread.Sleep(500) ' this sucks but what else can I do, actually it works well with google
            trynumber += 1
            Return GetGoogleLatLng(c, trynumber)
        End If
 
        Dim e As XElement = x.Descendants("geometry").Elements("location")(0)
        If Not e Is Nothing Then
            Return String.Format("{0},{1}", Xml.GetField(e, "lat", 25), Xml.GetField(e, "lng", 25))
        Else : Return String.Empty
        End If
 
    End Function 

 

 

BTW PS...

 

Want intellisense for your google. namespace?

 

I added this to the top of my usercontrol above

 

<%-- 
<% #if (false) %>
 <script src="../_scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
<% #endif %>
--%>
 

Tags:

Javascript | JQuery

Best of Datagrid plugins for web

by MikeHogg 12. August 2012 09:43

After researching several datagrid mechanisms for mvc3 web page, looking for powerfule filtering and sorting and paging built in, I went with actually a javascript implementation called SlickGrid.

 

https://github.com/mleibman/SlickGrid

 

There was another js grid actually that got very positive reviews also.  I think there was a SO question asking for reviews but it's been a while since I did this research and I don't remember the name or why I chose this one over the others, or over .net controls.  I just want to document how I used it for the future. It offers powerfully fast sorting paging and even As You Type filtering on datasets upwards of 100k I am told, and believe, although I have not had need to use it for more than an order of hundreds yet.

 

besides adding the source (i use slick.core, .dataview, .formatters (might be mine), and .pager) to your project, you init the grid like any other plugin.  You set your Columns and options.  Columns here is the important point.

 

 
<link href="@Url.Content("~/Content/slick.grid.css")" rel="stylesheet" type="text/css" />
<link href="@Url.Content("~/Content/slick.pager.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Content/js/jquery.event.drag-2.0.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.core.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.grid.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.dataview.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.formatters.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.pager.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/json2.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/js/slick.mylib.js")" type="text/javascript"></script>
 
<script type="text/javascript">
 
    var columns = [
     { id: "Name", name: "Name", field: "Name", width: 140, sortable: true },
                    { id: "Email", name: "Email", field: "Email", width: 140, sortable: true }, 
     { id: "Type", name: "Type", field: "Type", sortable: true }, 
                    { id: "Title", name: "Title", field: "Title", width: 150, sortable: true },
                    { id: "RequestDate", name: "Request Date", field: "RequestDate", formatter: Slick.Formatters.Date, filter: false, sortable: true },
                    { id: "DueDate", name: "Due Date", field: "DueDate", formatter: Slick.Formatters.Date, filter: false, sortable: true },
                    { id: "AnotherDueDate", name: "Another Due Date", field: "AnotherDueDate", formatter: Slick.Formatters.Date, filter: false, sortable: true },
                    { id: "Details", name: "Details", field: "Id", filter: false,
                        formatter: function (row, cell, value, columnDef, dataContext) {
                            return '<a href="/Home/ViewDetails/' + dataContext['Id'] + '">Details</a>';  }   },
 
                    { id: "selector", name: "Has Approval", field: "HasApproval", formatter: Slick.Formatters.Checkmark, cssClass: "centered", filter: false, sortable: true },
                    { id: "HasDeadline", name: "Has Deadline", field: "HasDeadline", formatter: Slick.Formatters.Checkmark, cssClass: "centered", filter: false, sortable: true },
                    { id: "AddFiles", name: "Add Files", field: "HtmlUploads", cssClass: "icons centered", filter: false,
                        formatter: function (row, cell, value, columnDef, dataContext) {
                            return '<span class="add-files" data-for="#add-files-text-' + dataContext['Id'] + '">Add</span>' +
                            '<span id="add-files-text-' + dataContext["Id"] + '" class="add-files-text">' +
                            value + '</span>'; }   },
 
                    { id: "HasCapital", name: "Has Capital", field: "HasCapital", cssClass: "centered", filter: false, sortable: true, formatter: Slick.Formatters.Checkmark },
                    { id: "DownloadFiles", name: "Download", field: "HtmlDownloads", width: 80, filter: false, formatter: function (row, cell, value, columnDef, dataContext) {
                        return '' + value; }   }
                ];
 
 
                var options = {
                    enableColumnReorder: false,
                    forceFitColumns: true,
                    defaultColumnWidth: 60,
                    autoHeight: true,
                    rowHeight: 60,
                    showHeaderRow: true,
                    headerRowHeight: 37
                };
 
</script>   
 
<h2>Welcome to the Mike Hogg Something Manager</h2>
 
<div class="text clearfix">
    <div class="button-cluster centered wide clearfix">
        @Html.ActionLink("Submit New Something", "NewSomething", null, null, new { @class = "btn" })
        @Html.ActionLink("View Calendar", "Index", "Calendar", null, new { @class = "btn" })
    </div>
    <br />
    <p>Below is a list of submitted somethings currently in your queue. To upload a xyz or abc file, select your something and click on the "Add File" icon. You can also sort the somethings by clicking on the column header, or filter by typing in  stuff</p>
</div> 
 
    <input type="hidden" id="message" value="used in getExport() above"/>
    <div id="myGrid"></div> 
    <div id="myPager"></div> 

 

And then in your js you set up your ajax method to get the json that populates the column, and sets the data to a placeholder control on your page.  I think leiberman builds the sort event into the grid, but you need to write your own comparers, so mine is here.  I think the basic filter was included but needs to be extended for your needs.  I had a need for escaping HTML and found the escape map also on S.O.

 

 

 
var grid;
var sortcol = "RequestDate";
var sortdir = -1;
var columnFilters = {};
var dataView = new Slick.Data.DataView({ inlineFilters: true });
dataView.setPagingOptions({ pageSize: 8 });
 
 
// my freshmen filter
function filter(item) {
    for (var columnId in columnFilters) {
        if (columnId !== undefined && columnFilters[columnId] !== "") {
            var c = grid.getColumns()[grid.getColumnIndex(columnId)];
            //if (item[c.field] != columnFilters[columnId]) {
 
            var field = item[c.field], myfilter = columnFilters[columnId];
            if (field != null && myfilter != null) {
                if (field.toString().toLowerCase().indexOf(myfilter.toString().toLowerCase()) == -1) {
                    return false;
                }
            } else { return field == myfilter; }
        }
    }
    return true;
}
 
// my freshmen attempt at comparer (for SORT)
function comparer(a, b) {
    var x = isNaN(a[sortcol]) ? a[sortcol].toLowerCase() : a[sortcol];
    var y = isNaN(b[sortcol]) ? b[sortcol].toLowerCase() : b[sortcol];
    if (x == null || x == "") {
        if (y == null || y == "") {
            return 0;
        }
        else return -1;
    } 
    else if (y == null || y == "") return 1;
    else return (x == y ? 0 : (x > y ? 1 : -1));
}
 
 
 
var entityMap = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': '&quot;',
    "'": '&#39;',
    "/": '&#x2F;'
};
 
function escapeHtml(string) {
    return String(string).replace(/[&<>"'\/]/g, function (s) {
        return entityMap[s];
    });
}
/// ADDITIONAL

 

We had some cute image links for a popup menu in one of the cells but as you scroll and page through the grid, they needed to be recreated as this grid implementation actually dropped and added rows on and off the dom on the fly, so LoadButtons ...

 

 
// for those pencil icon popup menus for Add Files
function loadButtons() {
 
    $(".add-files-text").dialog({
        autoOpen: false,
        title: "Add Files"
    });
    $(".add-files").button({
        icons: { primary: 'ui-icon-pencil' },
        text: false
    });
    $(".add-files").click(function () {
        $($(this).attr("data-for")).dialog("open");
        return false;
    });
}
 
 
// adds those filters to each column, special here to not add for certain columns, extended by 'filter:'
function updateHeaderRow() {
    for (var i = 0; i < columns.length; i++) {
        var header = grid.getHeaderRowColumn(columns[i].id);
        $(header).empty();
        if (columns[i].id !== "selector" && columns[i].filter != false) {
            $("<input type='text'>")
                    .data("columnId", columns[i].id)
                    .val(columnFilters[columns[i].id])
                    .appendTo(header);
        } else {
            $("<div><span style='height:39px;display:block;'></span></div>").appendTo(header);
        }
    }
    if (grid.getOptions()["excelExport"] == true) {
        // add Excel button
        var header = grid.getHeaderRowColumn(columns[columns.length - 1].id);
        $(header).empty();
        $("<input type='image' src='../Content/img/Excel-icon.png' title='Export to Excel' onclick='getExport();'/>").appendTo(header);
    }
}
 
 
 
function getExport() {
    $.ajax({
        url: '/Home/DeploymentList',
        type: "post",
        data: JSON.stringify(dataView.getFilteredRows()),
        dataType: "json",
        contentType: "application/json; charset=utf-8",
        success: function (result) { getCSVFileFromFormPost(result); },  // ajax post json data which server can read automatically, responds with a string, which ajax then POSTs inside a FORM, to which the server adds headers and returns, so browser can treat it as a download (save/open)
        error: function (xhr, textStatus, errorThrown) {
            $("#message").html('Error occurred, ReadyState: ' + xhr.readyState +
                '; textStatus: ' + textStatus + '; ' + errorThrown);
        }
    });
    return false;
}
 
function getCSVFileFromFormPost(result) {
    $('<form action="/Home/GetCSVFile" method="post"><input type="hidden" name="csv" id="csv" value="' + escapeHtml(result) + '" /></form>').appendTo("body").submit();
}
 

 

The Export to CSV function was a neat one, since we could send the dataset back to the server in json easily, and in MVC we could set up an Action to Deserialize that same json to a List of Models if we lined everything up right.

 

And "the call"...

 

$(function () {
    $.post('/Home/GetDepData', function (data, textstatus) {
 
        grid = new Slick.Grid("#myGrid", dataView, columns, options);
        var pager = new Slick.Controls.Pager(dataView, grid, $("#myPager"));
 
        grid.onSort.subscribe(function (e, args) {
            sortdir = args.sortAsc ? 1 : -1;
            sortcol = args.sortCol.field;
 
            if ($.browser.msie && $.browser.version <= 8) {
                // using temporary Object.prototype.toString override
                // more limited and does lexicographic sort only by default, but can be much faster
 
                dataView.fastSort(sortcol, args.sortAsc);
            } else {
                // using native sort with comparer
                // preferred method but can be very slow in IE with huge datasets 
                dataView.sort(comparer, args.sortAsc);
            }
        });
 
        // wire up model events to drive the grid
        dataView.onRowCountChanged.subscribe(function (e, args) {
            grid.updateRowCount();
            grid.render();
 
            loadButtons();  // when filtered to fewer rows than before and no rows were changed
        });
 
        dataView.onRowsChanged.subscribe(function (e, args) {
            grid.invalidateRows(args.rows);
            grid.render();
 
            loadButtons();  // when filtered or paged
        });
 
        dataView.onPagingInfoChanged.subscribe(function (e, pagingInfo) {
            var isLastPage = pagingInfo.pageNum == pagingInfo.totalPages - 1;
            var enableAddRow = isLastPage || pagingInfo.pageSize == 0;
            var options = grid.getOptions();
        });
 
 
        $(grid.getHeaderRow()).delegate(":input", "change keyup", function (e) {
            columnFilters[$(this).data("columnId")] = $.trim($(this).val());
            dataView.refresh();
        });
 
        // initialize the model after all the events have been hooked up
        dataView.beginUpdate();
        dataView.setItems(data, "Id");  // Id capital I to match my unique model id property
        dataView.setFilter(filter);
        dataView.endUpdate();
 
        updateHeaderRow();
        loadButtons();
    }, "json");
});

 

At the end of the included slick.formatters.js I added a couple of my own, the javascript time function being a keeper...

 

    ...
    function YesNoFormatter(row, cell, value, columnDef, dataContext) {
        return value ? "Yes" : "No";
    }
 
    function CheckmarkFormatter(row, cell, value, columnDef, dataContext) {
        return value ? "<img src='../Content/img/slickimages/tick.png'>" : "";
    }
    function DateFormatter(row, cell, value, columnDef, dataContext) {
        if (value !== null) {
            var d = new Date(parseInt(value.substr(6, 13)));
            return (d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear() + ' ' + getAMPMTime(d);
        } else return "";
    }
 
    Number.prototype.pad = function (len) {
        return (new Array(len + 1).join("0") + this).slice(-len);
    }
 
    function getAMPMTime(d) {
        var d = new Date(d);
        var hour = d.getHours();
        var min = d.getMinutes().pad(2);
        var ap = "AM";
        if (hour > 11) { ap = "PM"; }
        if (hour > 12) { hour = hour - 12; }
        if (hour == 0) { hour = 12; }
 
        if (hour == 12 && min == 0 && ap == "AM") return '';
 
        return hour + ':' + min + ' ' + ap;
    }

Tags:

JQuery | MVC | Javascript

JQuery

by MikeHogg 5. July 2012 09:49

 

On my third web project, I got the opportunity to dive into jquery a little more.   We were rewriting an existing site in MVC3.  One of my tasks was to reproduce a standard sort of “Locations” page.  I had some javascript to work from, but we were adding a new “Location Features” feature, and a GPS feature, and the existing javascript wasn’t in any shape to be extended.  I only needed a small form, and my only server side code, my Location Features MVC3 call, was a three liner LINQ query against an Entity Framework db, so most of the task was my opportunity to rewrite some old mess of long cryptic javascript function into jquery, and figuring out a good readable, maintainable code flow.    In the first page here you will see my standard style of using the MVC model binding, even for two properties, and a very short clean page of html, with my CSS file referenced in the beginning, and my JS file referenced at the end.

 

 

 

@model SomeBase.Models.LocatorModel
<link rel="stylesheet" href="@Url.Content("~/Content/css/locator.css")">
<div class="locatordiv">
    @using (Html.BeginForm("Locator", "Home", FormMethod.Post, new { id = "locator" }))
    {   
        <p>  Enter a city and state, or zip code below.</p>
                    
        @Html.HiddenFor(m => m.Gps)
        @Html.TextBoxFor(m => m.Zip, new { title = "Enter ZIP", @Class = "textbox" })
        
        <input type="submit" value="" class="findbutton" data-category="Find" data-event="Homepage Zipcode" />
        <a href="#" class="gpsbutton" onclick="do_geo();" title="GPS" data-category="Find" data-event="GPS"></a>
     
        <div id="gpsloading">Working ...<br /><img src="@Url.Content("~/Content/images/load-bar.gif")" /></div>     
    }  
</div>
<div id="maploading">
    <img src="@Url.Content("~/Content/images/load-circle.gif")" />
</div>
<div id="results"></div>     
<div id="mapDiv"></div>
    
<script type="text/javascript">
            var configMQAPIKey = '@Html.Raw(System.Configuration.ConfigurationManager.AppSettings["MQ:APIKey"].ToString())';
            var configMQOLOKey = '@Html.Raw(System.Configuration.ConfigurationManager.AppSettings["MQ:OLOKey"].ToString())';
            var configMQHostedDataTable = '@Html.Raw(System.Configuration.ConfigurationManager.AppSettings["MQ:HostedDatatable"].ToString())'; 
        </script>
        <script src='//www.mapquestapi.com/sdk/js/v7.0.s/mqa.toolkit.js?key=@Html.Raw(System.Configuration.ConfigurationManager.AppSettings["MQ:APIKey"].ToString())'></script>
        <script src="@Url.SomeContent("~/Content/js/locator.js")"></script>
        <script type="text/javascript" src="@Url.Content("~/Content/js/jquery.cookie.js")" ></script> 

The javascript wasn’t terribly complicated, but it was fun to get deeper into jquery and learn to use standard jquery element creation and manipulation methods, and the unobstrusive javascript pattern, as opposed to the old verbose javascript getelementById calls.   Here you will see my style of paying special attention to method and variable names, in place of comments.  I was taught that comments should be for WHY not WHAT, and if you name your objects clearly enough, you don’t need to comment WHAT you are doing.

 

 

function searchByGps() {
    $("#Zip").val(""); $("#Key").val("")
    search("https://www.mapquestapi.com/search/v1/radius" +
                    "?key=" + mq_key + "&radius=" + $("#inradius").val() + "&callback=processPOIs&maxMatches=" + inmatch +
           "&origin=" + encodeURIComponent($("#Gps").val()) + "&hostedData=" + intable);
} 
function searchByZip() {
    $("#Gps").val(""); $("#Key").val("")
    search("https://www.mapquestapi.com/search/v1/radius" +
                    "?key=" + mq_key + "&radius=" + $("#inradius").val() + "&callback=processPOIs&maxMatches=" + inmatch +
           "&origin=" + encodeURIComponent($("#Zip").val()) + "&hostedData=" + intable);
} 
                
function search(url){
    MQA.IO.doJSONP(url); 
}
function processPOIs(results) {
    if (results.searchResults != null && results.searchResults.length > 0) {
        drawPOIsOnMap(results.searchResults);
        drawResultTable(results.searchResults);
        
        if ($("#Key").val()) {
            $("#Zip").val(results.searchResults[0].fields.address + " " + results.searchResults[0].fields.city + ", " + 
                          results.searchResults[0].fields.state + " " + results.searchResults[0].fields.postal); 
        }
        $.ajax('/Home/GetLocationFeatures', {
            data: JSON.stringify(parseToEntityObjects(results.searchResults)),
            dataType: "json",
            type: "post",
            contentType: "application/json",
            success: function (featuredata) { processFeatures(featuredata, results.searchResults); }
        });
    } 
    else if (results.info && results.info.statuscode == 610) {
        // ambiguities
        // results.collections[1] is To, 0 is From
        $("#Gps").val(results.collections[0][0].latLng.lat + ',' + results.collections[0][0].latLng.lng);
        searchByGps();
    }
    else {
        $('#results .frame').html("<h1>Oops! We couldn’t find any results. Please try your search again.</h1>");
    }
}
function processOLOs(oloData, searchResults) {
    $.each(searchResults, function (i, result) { 
        if (oloData != null) {
            $.each(oloData.restaurants, function () {
                if (this.telephone == result.fields.Phone) {
                    var oloid = '#olo' + result.fields.RecordId;
                    $(oloid).append($("<a></a>", { href: this.url, "class": "ololinks", target: "_blank", text: "Place an Order" }));
                }                    
            });  }   }); }  
function processFeatures(featureData, searchResults) {
    $.each(searchResults, function (i, result) { 
        if (featureData != null) {
            $.each(featureData, function () {
                if (this.StoreNumber == result.fields.RecordId) {
                    var fid = '#feature' + result.fields.RecordId;
                    $(fid).append($("<a></a>", {href:this.Url, "class":"featurelinks", target:"_blank", text:this.Label}));
                }   });         }  });  }   
function drawResultTable(results) {
    $("#maploading").hide();
    $('#results').html("<h1>Search Results</h1>");
         
    $.each(results, function (i, result) {
            
        $("<div></div>", { "class": 'resultrow' })
                     .append("<p class='addressrowresult'>" +
                result.fields.address + "<br/ >" +
                result.fields.city + ", " + result.fields.state + "<br/ >" +
                result.fields.Phone + "<br /></p>")
            .append("<div>" + getRoundedDistance(result) + " Mi.    " + getMapItLink(result) + "</div>") 
            .append("<div class='olos' id='olo" + result.fields.RecordId + "'></div>")   // olo can find this span$(#olo#storenumber#) later
            .append("<div class='features' id='feature" + result.fields.RecordId + "'></div>") // feature can find this span$(#feature#storenumber#) later
            .appendTo("#results");
    }); }  
function parseToEntityObjects(data) {
    var locations = [];
    $.each(data, function () {
        locations.push({
            StoreNumber: this.fields.RecordId,
            Address1: this.fields.address,
            City: this.fields.city,
            ZipCode: this.fields.postal,
            Phone: this.fields.Phone
        });
    });
    return locations;
}
function getRoundedDistance(result) { 
    if (result.distance > 10) {
        return Math.round(result.distance);
    } else if (result.distance > 1) {
        return Math.round(result.distance * 10) / 10;
    }  
    return Math.round(result.distance * 100) / 100; 
}
function getMapItLink(result){ 
    var destination = result.fields.address + ',' + result.fields.city + ',' + result.fields.state + ',' + result.fields.postal;
    var link = $("<a>", { href: "#", "class": "mapitlink", title: "Map It!", onclick: "mapIt(getOrigin(), '" + destination + "');return false;", text: "Map It!" })
                .attr({ "data-category": "Find", "data-event": "Map Quest" });
    return $('<div>').append(link.clone()).html();  // hack to get string not js object for mqa
}                    
function getPOIRollover(result) {
    var rollover = $("<div></div>", { "class": "poirollover" }).append($('<h4></h4>').text(result.fields.N))
        .append($('<span></span>').text(result.fields.address))
        .append($('<br />')).append($('<span></span').text(result.fields.city + ", " + result.fields.state))
        .append($('<br />')).append($('<span></span>').text(result.fields.Phone))
        .append($('<br />')).append($('<span></span>').text(getRoundedDistance(result) + " Mi.  "))
        .append(getMapItLink(result));
    return $('<div>').append(rollover.clone()).html(); // hack to get string not js object for mqa
}
function mapIt(origin, destination) {
    // build a url and send to the Directions Web Service
    MQA.IO.doJSONP("http://www.mapquestapi.com/directions/v1/route?" +
              "key=" + mq_key + "&" +
              "from=" + origin + "&" +
              "to=" + destination + "&" +
              "shapeFormat=raw&generalize=0.1&" +
              "callback=drawDirections");
}

 

My CSS file was mostly empty at the start.  But I wanted to make it easy to add style later, by applying classes and these empty placeholders for just about every element that we might want.

 

 

.locatordiv
{
    text-align:center; 
    margin:20px;
} 
.gpsbutton {
       background:url(../images/icon-gps.png) no-repeat;      
       background-size:42px auto; 
       padding:21px;    
} 
#gpsloading
{    display:none;
}
#maploading
{     display: none;  
}
#mapDiv
{    float:left; 
}
#results
{    float:left;
}
.resultrow
{    margin:20px;
}
.mapitlink
{    
}
.poirollover
{    width:150px;
}
.olos
{
}
.ololinks
{
}
.features
{    display:block;
}
.featurelinks
{    display:block;
}
.addressrowresult
{    width: 210px;
}
#directionsdiv
{    
}
.directionrow
{    
}

Tags:

Javascript | JQuery

About Mike Hogg

Mike Hogg is a c# developer in Brooklyn.

More Here

Favorite Books

This book had the most influence on my coding style. It drastically changed the way I write code and turned me on to test driven development even if I don't always use it. It made me write clearer, functional-style code using more principles such as DRY, encapsulation, single responsibility, and more. amazon.com

This book opened my eyes to a methodical and systematic approach to upgrading legacy codebases step by step. Incrementally transforming code blocks into testable code before making improvements. amazon.com

More Here