Category Archives: JavaScript

e.target vs. e.currentTarget

I feel the need to explain the difference between e.target and e.currentTarget. There are subtle differences. Relying on e.target is NOT the way to go, and I tend to see over usage amongst the code that I review.

Consider the following markup:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <title>Untitled Page</title>
    <style type="text/css">
        a {
            background: pink;
            padding: 10px;
        }
        span {
            background: yellow;
        }
    </style>
</head>
<body>
    <a href="#"><span>This is some content inside a link</span></a>
</body>
<script type="text/javascript" src="/Scripts/jquery-1.7.1.min.js"></script>
<script type="text/javascript" src="/Scripts/jquery.cookie.js"></script>
<script type="text/javascript" src="/Scripts/jquery.querystring.js"></script>
<script type="text/javascript">
    var debug = 'on';
</script>
<script type="text/javascript" src="/Scripts/log.js"></script>
<script type="text/javascript">
    $(function () {
 
        $('a').on('click', function (e) {
            /// <param name="e" type="jQuery.Event">jQuery Event Object</param>
            var target = e.target,
                $target = $(target),
                currentTarget = e.currentTarget,
                $currentTarget = $(currentTarget);
 
            $target.log('I am the target');
            $currentTarget.log('I am the currentTarget');
        });
 
    });
</script>
</html>

This renders the page like this:

If I click on the pink padding of the a, e.target and e.currentTarget are predictably the a.

If I click on the actual link which is really INSIDE the span, e.target and e.currentTarget are completely different.

The SPAN that you clicked on is the target. However, we bound the original event handler to the A. So if you wanted your event handler callback function to operate in reference to the A, you’re shit out of luck that you chose e.target in your code.

You might be asking, why not use “this”? I personally tend to shy away from using “this”, because you have to pay special attention to scope and closures. If you know what I’m talking about, then go right ahead and use “this”.

For arguments sake, here is the output when we add it to the code:

    $(function () {
 
        $('a').on('click', function (e) {
            /// <param name="e" type="jQuery.Event">jQuery Event Object</param>
            var target = e.target,
                $target = $(target),
                currentTarget = e.currentTarget,
                $currentTarget = $(currentTarget),
                $this = $(this);
 
            $target.log('I am the target');
            $currentTarget.log('I am the currentTarget');
            $this.log('I am this');
        });
 
    });

KNOW THIS!

A special note, I am using the scripts log.js and jquery.cookie.js to do some special logging stuff. You may have notice that I had a line of code that said var debug = ‘on’. Well, if this wasn’t set, I also could have added debug=on to my uri querystring and also turned on console logging. If you’re interested in those little tidbits, you can check out the attached project.

Download the content here

Advertisements

Develop for one, deploy to all – HTML5 and your mobile apps

I had the great pleasure of returning to my native California to attend the DevCon5 conference in Silicon Valley last week.  The primary focus was HTML5.  If you don’t know what HTML5 is, have a look at Wikipedia’s definition.  I’m going to give you a paraphrased definition that Paul Trani of Adobe provided:

HTML5 is not just markup.  HTML5 encompasses the new HTML doctype and associated HTML markup, CSS3 and JavaScript.

The conference was really like most others that I had been to, where it was hit or miss depending on which sessions you chose to go to.  The main takeaway was in the mobile development arena.  What the speakers generally were proposing was that you should develop your mobile applications using HTML5 using device native web views or use a wrapper like PhoneGap.  The benefit here really is develop for one, deploy to all.

It just so happens that on the plane ride back to Minneapolis, the traveler sitting next to me was Marc Grabanski of jQueryUI datepicker fame.  We talked about imperative vs. declarative js and his new company MJG International where he is doing exciting things with video training at Frontend Masters.  I took the opportunity of this chance meeting to share my experiences of the last few days regarding HTML5.

Here are a few points that I garnered:

  • His general feeling was that if you are going to wrap your mobile apps, don’t use PhoneGap… use Titanium.  Titanium provides a much deeper hook into a device’s built-in features.
  • Don’t use Sencha Touch.  Instead, use jQuery.mobile for your phone development.  While Marc seemed to be a jQuery.mobile evangelist, I think he has a good point.  Sencha was built for a desktop application developer in mind, NOT a web developer.
  • If you want to succeed, you must believe through and through in what you are doing. (while this is not developer related, I though it must be shared)  Personally, I would emphasize this point and say “let your passion run wild”.
  • Develop Mobile First.  Or even, Mobile For All.  Marc was showing me how he was using jQuery Mobile to develop an entire web site, basically using it as a templated website that scaled depending on device size.

I was also happy to discuss my recent blog post about jQuery UI unobtrusive with ajax support.  Based on that conversation, I’m probably going to do a re-write based on how Keith Wood’s Clean Calendar was doing the wiring up of the options.

All in all it was a great trip.  What I really want to do now however, is code…

jQuery Inline Editor plugin (source code)

Yesterday I talked about the inline editing paradigm that we are using for WebCenter 6. I have upgraded the plugin to follow the best practices for authoring a jQuery plugin.  The plugin now takes in methods and options.  I have exposed the methods “edit”, “cancel”, and “destroy” to allow for calling the edit and cancel functionality from outside the core function.  My next step will be to call the inline editing from within unobtrusive ajax onsuccess methods which I talked about before.

/*!
 * jQuery Inline Editor
 * Copyright 2011, Andrew Cohen https://omegaluz.wordpress.com
 */

(function ($) {

    var getSettingsElements = function (settings, context) {
        var display = $(settings.display, context);
        var edit = $(settings.edit, context);
        var editlink = $(settings.editlink, context);
        var cancellink = $(settings.cancellink, context);
        return {
            display: display,
            edit: edit,
            editlink: editlink,
            cancellink: cancellink
        };
    }

    /// <param name="$" type="jQuery">jQuery Object</param>
    var methods = {
        init: function (options) {

            return this.each(function () {

                var $this = $(this),
                     data = $this.data('inlineeditor'), // retrieve the settings that were previously stored
                     events,
                     settings;

                // If the plugin hasn't been initialized yet
                if (!data) {

                    var defaults = {
                        display: '[data-inlineeditor-display]',
                        edit: '[data-inlineeditor-edit]',
                        editlink: '[data-inlineeditor-editlink]',
                        cancellink: '[data-inlineeditor-cancellink]'
                    };

                    settings = $.extend(defaults, options);

                    events = {
                        editClick: function (e) {
                            /// <param name="e" type="jQuery.Event">jQuery Event Object</param>
                            e.preventDefault();

                            var $settings = getSettingsElements(settings, $this);
                            $settings.display.add($settings.editlink).hide();
                            $settings.edit.add($settings.cancellink).show();
                        },
                        cancelClick: function (e) {
                            /// <param name="e" type="jQuery.Event">jQuery Event Object</param>
                            e.preventDefault();

                            var $settings = getSettingsElements(settings, $this);
                            $settings.edit.add($settings.cancellink).hide();

                            var $form = $this.find('form');
                            $.validator.unobtrusive.parse($form);
                            if ($form.length > 0) {
                                var validator = $form.data('validator'); // get the jQuery.validate validator object for the form
                                if (validator) {
                                    validator.resetForm(); // reset the form using the validator
                                } else {
                                    $form[0].reset(); // reset using the form's built-in reset function
                                }
                            }

                            $settings.display.add($settings.editlink).show();
                        }
                    };
                    data = {
                        settings: settings,
                        events: events
                    };
                    // save the settings in the data
                    $(this).data('inlineeditor', data);

                } else {
                    settings = data.settings;
                    events = data.events;
                }

                // hide both the edit elements and the cancel link
                var $settings = getSettingsElements(settings, $this);
                $settings.edit.add($settings.cancellink).hide();

                // wire up the events
                $this.on('click.inlineeditor', settings.editlink, events.editClick);
                $this.on('click.inlineeditor', settings.cancellink, events.cancelClick);

            });
        },
        destroy: function () {
            return this.each(function () {

                var $this = $(this),
                     data = $this.data('inlineeditor');
                if (data) {
                    var settings = data.settings;
                    $this.off('.inlineeditor');
                    $this.removeData('inlineeditor');
                }

            })
        },
        edit: function () {
            return this.each(function () {

                var $this = $(this),
                    data = $this.data('inlineeditor');
                if (data) {
                    var settings = data.settings;
                    var $settings = getSettingsElements(settings, $this);
                    $settings.editlink.trigger('click');
                }

            })
        },
        cancel: function () {
            return this.each(function () {

                var $this = $(this),
                     data = $this.data('inlineeditor');
                if (data) {
                    var settings = data.settings;
                    var $settings = getSettingsElements(settings, $this);
                    $settings.cancellink.trigger('click');
                }

            })
        },
        close: function (context) {
            return this.each(function () {

                var $this = $(this),
                    data = $this.data('inlineeditor');
                if (data) {
                    var settings = data.settings;
                    var $settings = getSettingsElements(settings, $this);
                    $settings.edit.add($settings.cancellink).hide();
                    $settings.display.add($settings.editlink).show();
                }
            })
        },
        closeWhenValid: function () {
            return this.each(function () {

                var $this = $(this),
                     data = $this.data('inlineeditor');
                if (data) {
                    var $form = $this.find('form');
                    $.validator.unobtrusive.parse($form);
                    if ($form.length > 0) {
                        if ($form.find('.input-validation-error').length == 0) {
                            methods.close.apply($this, arguments);
                        }
                    }
                }

            })
        }
    };

    $.extend($.fn, {
        inlineeditor: function (method) {
            if (methods[method]) {
                return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
            } else if (typeof method === 'object' || !method) {
                return methods.init.apply(this, arguments);
            } else {
                $.error('Method ' + method + ' does not exist on jQuery.inlineeditor');
            }
        }
    });

} (window.jQuery));

Inline Editing paradigm using a jQuery Inline Editor plugin

As our WebCenter 6 project is nearing release, we’re constantly trying to make our code more DRY.  The basic premise of this post is this: how do we duplicate the showing and hiding of elements needed for inline form field editing without re-inventing the wheel.

What is this inline editing that I’m speaking of?  Well, consider this content:

Customer details pane from WebCenter 6

Customer details pane from WebCenter 6

Rather than creating a whole new page, to edit the Username and other information, why not just pop an edit link next to the Username and allow editing right there on the page?  This eliminates the need for creating a second page, popup, or what have you.  When the edit link is clicked, the display of the username turns into an editor.  An associated cancel link and Save button would also appear.

username edit link

username edit link

editor, save, and cancel link

editor, save, and cancel link

I can accomplish this pretty easily with some nifty jQuery code using hide, show, toggle, etc., but what I really want to do is write this code once and be able to re-use it for other forms that would use the exact same functionality.

Enter the jQuery plugin.

Our wonderfully talented UX developer, Pete Klein suggested we wire this up with data attributes.  Without delving into the details of writing a jQuery plugin, or really optimizing the code, here it is:

/*!
 * jQuery Inline Editor
 * Copyright 2011, Andrew Cohen https://omegaluz.wordpress.com
 */

(function ($) {
    /// <param name="$" type="jQuery">Description</param>

    $.fn.inlineeditor = function (options) {

        var $this = this;

        var settings = $.extend({
            display: this.find('[data-inlineeditor-display]'),
            edit: this.find('[data-inlineeditor-edit]'),
            editlink: this.find('[data-inlineeditor-editlink]'),
            cancellink: this.find('[data-inlineeditor-cancellink]'),
            form: this.find('form[data-val=true]')
        }, options);

        settings = $.extend(settings, this.data('settings')); // apply the settings that are stored in the data['settings'] object
        this.data('settings', settings); // store the current settings in the settings data object
        this.attr('data-inlineeditor', 'true');
        settings.edit.hide();
        settings.cancellink.hide();

        settings.editlink.on('click', function (e) {
            /// <param name="e" type="jQuery.Event">jQuery Event Object</param>
            var target = e.target;
            var $target = $(target);
            var currentTarget = e.currentTarget;
            var $currentTarget = $(currentTarget);

            e.preventDefault();

            settings.display.hide();
            settings.editlink.hide();

            settings.edit.show();
            settings.cancellink.show();
        });
        settings.cancellink.on('click', function (e) {
            /// <param name="e" type="jQuery.Event">jQuery Event Object</param>
            var target = e.target;
            var $target = $(target);
            var currentTarget = e.currentTarget;
            var $currentTarget = $(currentTarget);

            e.preventDefault();

            settings.edit.hide();
            settings.cancellink.hide();

            var $form = $this.find('form');
            if ($form.length > 0) {
                var validator = $form.data('validator');
                if (validator) {
                    validator.resetForm();
                }
            }

            settings.display.show();
            settings.editlink.show();
        });
        /*// is the below code more efficient?
        this.on('click', settings.editlink.selector, function (e) {
        /// <param name="e" type="jQuery.Event">jQuery Event Object</param>
        e.preventDefault();
        settings.display.hide();
        settings.editlink.hide();
        settings.edit.show();
        settings.cancellink.show();
        });
        this.on('click', settings.cancellink.selector, function (e) {
        /// <param name="e" type="jQuery.Event">jQuery Event Object</param>
        e.preventDefault();
        settings.edit.hide();
        settings.cancellink.hide();
        settings.display.show();
        settings.editlink.show();
        });
        */

    };

} (window.jQuery));

Interesting to note, I’m making use of the new jQuery.on function instead of using click, bind, or live.

This code assumes the following:

  • Plugin and all associated functionality are instantiated like so: $(‘#sampleeditor’).inlineeditor();
  • There is an encapsulating element from which all other elements are nested.
  • Within the root there exists ‘[data-inlineeditor-display]’, ‘[data-inlineeditor-editlink]’, ‘[data-inlineeditor-edit]’, ‘[data-inlineeditor-cancellink]’.
  • ‘[data-inlineeditor-display]’ is an element that contains the original text
  • ‘[data-inlineeditor-editlink]’ is a link that when clicked hides the display and shows the editor
  • ‘[data-inlineeditor-edit]’ is a form input that serves to edit the content
  • ‘[data-inlineeditor-cancellink]’ cancels out of the editor, reset’s the form, hides the editor and shows the display again

My mvc 3 razor/html markup looks like so:

        <li><span class='key lfloat'><strong>Username:</strong></span>
            <span class='value lfloat' id="usernameeditor">
                <span data-inlineeditor-display>@Model.User.UserName</span>
                <a data-inlineeditor-editlink href="#">Edit</a>
                <span data-inlineeditor-edit id='usernameEdit'>
                    @{
                        Html.RenderPartial("RenameUserControl", Model.UserRenameViewModel);
                    }
                </span>
                <a data-inlineeditor-cancellink href="#">Cancel</a>
            </span>
            <script type="text/javascript">
                $(function () {
                    $('#usernameeditor').inlineeditor();
                });
            </script>
            <div class='clear'>
            </div>
        </li>

The content of the RenameUserControl:

@model UserRenameViewModel

@using (Ajax.BeginForm("RenameUser",
    "Home",
    null,
    new AjaxOptions
    {
        HttpMethod = "POST",
        UpdateTargetId = "usernameEdit"
    },
    new { id = "renameUserForm" }))
{
        @Html.HiddenFor(model => model.UserID)
        @Html.HiddenFor(model => model.UserName)
        <span class="editor-field">
            @Html.EditorFor(model => model.NewUserName)
            @Html.ValidationMessageFor(model => model.NewUserName)
        </span>

        @Html.ValidationSummary(true)

        <input type="submit" value="Save" />
}

<script type="text/javascript">
    $(function () {
        $.validator.unobtrusive.parse('#renameUserForm');
    });
</script>

Interesting to note, we’re using MVC 3’s built in unobtrusive ajax with data annotations to accomplish the actual form submission.  Again, I didn’t have to write any javascript to wire that up.

I can now re-use this plugin wherever we’re doing inline editing on our site.  I’ll still need to hook into my client side and server side validation when I’m clicking the save button.  I’ll probably wire up an onsuccess event to check whether there are any validation errors and then if not, fire some kind of hide event, rather than the plugin’s cancel code.

** I have updated the source for this. You can find it here.

Unobtrusive jQuery UI with AJAX support

Recently I came across a post by Joe Stagner that talked about the practice of “Unobtrusive JavaScript” in web pages.  Assuming that you understand the separation of concerns therein, I’m going to jump right to a jQuery plug-in authored by Damian Edwards that is available on NuGet, Unobtrusive jQuery UI.

The main idea is this: you can add markup to your html content that wires up jQuery UI without the need to write any additional javascript; just by virtue of including the plug-in script in your html, you can wire up jQuery UI widgets.

Normally, to wire up the following:

jQuery DatePicker

jQuery DatePicker

You would need to write this code:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <link href="Content/themes/base/minified/jquery-ui.min.css" rel="stylesheet" type="text/css" />
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery-ui-1.8.16.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        $(function () {
            $('#date').datepicker({});
        });
    </script>
</head>
<body>
    <input type="text" name="date" id="date" />
</body>
</html>

The plug-in he wrote is fantastic in that it does the job with a minimal amount of effort.  But, it doesn’t address any content that was added AFTER the page has loaded via AJAX.  This is because his plugin wires everything up in jQuery’s document.ready event.  The other limitation to his code is that it did not correctly parse options for jQuery UI widgets that were camelCased.

So if there was an option like changeMonth, the attribute should be data-ui-datepicker-changeMonth.  However, the browser will render this as data-ui-datepicker-changemonth without the uppercased M.  This poses a problem in that JavaScript is a case-sensitive language and that option will be passed to the jQuery UI widget function like:

    <input type="text" name="date" id="date" data-ui-fn="datepicker" data-ui-changeMonth="true" />

will functionally get evaluated as:

    $('#date').datepicker({
        changemonth: true
    });

When in fact what we need is:

    $('#date').datepicker({
        changeMonth: true
    });

Check out his source:

/*!
 * Unobtrusive jQuery UI 0.1
 * Copyright 2011, Damian Edwards http://damianedwards.com
 * Licensed under Ms-PL
 * http://www.opensource.org/licenses/MS-PL
 */
(function ($) {
	"use strict";

    $(function () {
        var prefix = "data-ui-";

        // Wire-up jQuery UI unobtrusively
        $("*[" + prefix + "fn]").each(function () {
            var el = this,
                $el = $(el);

            // Loop through functions in data-ui-fn attribute
            $.each($el.attr(prefix + "fn").split(" "), function () {
                var fn = this,
                    options = {},
                    optionPrefix = prefix + fn + "-";

                // Build options parameter from data-ui-[fn]-* attributes
                $.each(el.attributes, function () {
                    var attr = this;
                    if (!attr.specified) return true;

                    if (attr.name.indexOf(optionPrefix) === 0) {
                        options[attr.name.substr(optionPrefix.length)] = attr.value;
                    }
                });

                // Call jQuery UI fn if it exists
                ($el[fn] || $.noop).call($el, options);
            });
        });
    });

}(window.jQuery));

Yesterday I proposed an update that would do the following:

  1. Add a method that allowed to parse the an element an all of it’s children in the hierarchy of the DOM
  2. Add a method that parses a specific element in the DOM
  3. Correct the parsing of the options for the jQuery UI widget function.  Fix camel casing.

Here is what I came up.  You should note, that I modeled this after the default jquery.validate.unobtrusive.js code that is in any default MVC project.

/*!
* Unobtrusive jQuery UI 0.1
* Copyright 2011, Damian Edwards http://damianedwards.com
* Licensed under Ms-PL
* http://www.opensource.org/licenses/MS-PL
*/

/*!
* Modified by Andrew Cohen, 11/30/2011
* TempWorks Software, Inc.
*/

(function ($) {
    "use strict";

    var $jQui = $.ui,
        prefix = "data-ui-";

    $jQui.unobtrusive = {
        parse: function (element) {
            // Wire-up jQuery UI unobtrusively
            $("*[" + prefix + "fn]", element).each(function () {
                $jQui.unobtrusive.parseElement(this);
            });
        },
        parseElement: function (el) {
            var $el = $(el),
                el = $el[0];

            // Loop through functions in data-ui-fn attribute
            $.each($el.attr(prefix + "fn").split(" "), function () {
                var fn = this,
                    options = {},
                    optionPrefix = prefix + fn + "-";

                // Build options parameter from data-ui-[fn]-* attributes
                $.each(el.attributes, function () {
                    var attr = this;
                    if (!attr.specified) return true;

                    if (attr.name.indexOf(optionPrefix) === 0) {
                        // camelCase the name
                        var attrName = $.camelCase(attr.name.substr(optionPrefix.length));
                        options[attrName] = attr.value;
                    }
                });

                // get UI fn if it exists
                var uiFn = ($el[fn] || $.noop);
                // call destroy to remove the ui widget
                uiFn.call($el, 'destroy');
                // call fn with options
                uiFn.call($el, options);
            });
        }
    }

    $(function () {
        $jQui.unobtrusive.parse(document);
    });

} (window.jQuery));

It also has 2 new methods:

$.ui.unobtrusive.parse

and

$.ui.unobtrusive.parseElement

The two new methods allow you to re-parse the DOM after content has been added to your page via AJAX.

I have submitted this change to Damian’s GitHub repository.