RSS
 

Posts Tagged ‘jquery’

ES6 – a quick look at Weak Maps

31 Oct

One of my favourite proposals in ECMAScript Harmony is Weak Maps. Simply put, ‘weak maps are unordered tables mapping objects to values’ with references to those objects and values being weak. A weak reference means that the item won’t be protected from garbage collection if no other references are held. For example:

// Using the proposed API (currently implemented in Firefox)
var wm = WeakMap();

(function () {
    var obj = {};
    wm.set(obj, 1);
})();

console.log(wm); //-> "[object WeakMap]"

By now a very obvious use case might have occurred to you, but I’ll get to that in a minute. When the above function finishes execution, obj is no longer reachable, so it can be garbage collected even though our WeakMap object is still reachable. This addition to the language is a useful tool for an age-old concern in JavaScript and the DOM; memory leaks. That aforementioned use case is becoming more obvious now, eh?

Where do Weak Maps fit in with current code?

OK, if you never had to implement this kind of thing before, you might not have thought of jQuery’s data() method. jQuery.data() allows us to attach an arbitrary value to a DOM object. Whilst you can do this with expando properties, ordinarily you run the risk of circular references and memory leaks. jQuery tackles this by creating a unique ID for itself (only once) and each element it is attaching data to. It tacks the element’s unique ID, which is really just an incremented counter, onto an expando property whose name is the unique ID. So it looks something like this:

     element[jQuery.expando] = elementId;

Any data you attach to the element is actually stored in an array at index elementId. jQuery is able to avoid memory leaks because there are no circular references, and it can delete all stored data when the page is unloaded (or the element is removed using jQuery’s own methods). However, it does have some limitations. For example — according to the source code — object/embed/applet cannot accept expando properties and, therefore, jQuery.data() cannot work with them.

Weak maps come in here because they can do the job much better. They cut out the need for the expando property entirely, along with the requirement of handling JS objects differently to DOM objects. They also expand on jQuery’s ability to allow garbage collection when DOM elements are removed by its own methods, by automatically allowing garbage collection when DOM elements no longer reachable after they’ve been removed by any method. Had WeakMaps been implemented in ECMAScript 3 (if only), jQuery’s workload with data() could have been significantly reduced. Perhaps jQuery wouldn’t have bothered implementing data if we could create weak maps, though.

Another use case to consider is secretly extending objects without the danger of naming collisions. Using someGloballyAvailableObject.someProperty = "some arbitrary value"; as an example, what if someGloballyAvailableObject already has someProperty? We’ve just written a potentially script-breaking piece of code. However, with a weak map we can eliminate this danger entirely:

(function () { 
    var wm = new WeakMap();
    wm.set(someGloballyAvailableObject, {someProperty:"some arbitrary value"});
})();

There’s also a hidden bonus here. I used the word secretly before because no other piece of our script can access this mapped value if we never expose the wm variable.

You might recall (if you read it) my article on creating lookup lists. If you do, you might be thinking that weak maps are a good use for creating lists, but in reality this isn’t the case because weak maps — in the current proposal, at least — will only map a value to an object.

Weak map compatibility shims

A true weak map is technically impossible in ECMAScript 3. We can create a map of objects to values by simply using two arrays:

function WeakMap() {
    this.keys = [/* key01, key02, key03 */];
    //                ↓      ↓      ↓
    this.vals = [/* val01, val02, val03 */];
}

Unfortunately, there’s 2 very important caveats here. Firstly, these references are not (and cannot be) weak, so they will not be garbage collected as long as the mapping exists. Secondly, for our get(key) method to work, we need to search our keys array. This significantly impacts performance, particularly on large maps.

As for ES5, I was pointed in the direction of this implementation recently. It’s very clever; It eliminates the need for a keys array and uses a custom valueOf function to keep track of the mapped value. I profiled the method in Chrome and it does indeed appear to prevent leaks. However, it does still have its limitations like not working on frozen objects or objects whose valueOf function is not configurable.

 
 

Update: HTML5 oninput event plugin for jQuery

24 Oct

About a year ago, I wrote my first special events plugin for jQuery — a cross-browser implementation of the HTML5 oninput event. A while afterwards, though, it became apparent that there were a multitude of issues with browsers that already implemented the event.

  • Opera 10.x doesn’t fire the event for cut, paste, undo & drop operations on <input> elements. They fixed this in Opera 11, though.
  • Safari 4.x and lower don’t support the event for <textarea> elements. The textInput event is supported on them for 3.1+, however.
  • Internet Explorer 9 forgets to fire the event when the backspace key is used to delete text. Other than that majorly horrendous oversight, they almost got it right.
  • Firefox has issues that make feature detection very difficult. In fact, detection is only possible via another Firefox proprietary feature, which I suppose is rather lucky.

With these problems stacking up, my code was stacking up too — how could I detect and work around each of these problems? On top of that, I couldn’t easily make the event work with delegate() or live(). Soon, it became apparent that I was going to have to completely rethink the approach I made in the original plugin.

Feature detection

The original code used feature detection to discover whether browsers supported the event. "oninput" in document.documentElement is enough for this in most browsers, but the aforementioned Firefox issues required an excellent little snippet from Daniel Friesen, which I slightly modified. It was a shame though, as this extra work bulked up the plugin a little more than I would have liked. Safari didn’t like our feature detection either, because even though our snippet told us the browser implemented support for the event, it doesn’t tell us that <textarea> won’t fire it.

I eventually decided that feature detection wasn’t going to work — graceful degradation, on the other hand, might. This involves binding to all known events that fire when an input element is modified, and then setting a flag to tell the event handlers whether or not they need to execute. For events that fire pre-DOM modification, we set a timer with timeout value of 0ms (this will be adjusted to browser’s minimum allowable value). The events that fire post-DOM modification cancel this timer. So, let’s say our browser is Internet Explorer 6, we have some text in our clipboard and we just pressed Ctrl+V in a <textarea> that has our events bound. For the Ctrl key, the onkeydown event sets the timer but when it executes the value hasn’t changed, so the handler does nothing. For the V key, the plugin will handle the events in the following sequence:

Pre-DOM modification
    onkeydown -> timer is set
    onpaste   -> previous timer is cleared, new timer is set

Post-DOM modification - DOM is modified, textarea's value is updated but element hasn't repainted yet
    onpropertychange -> timer is cleared, the handler is triggered

Opera 9, however, the best possible compatibility we can provide is via the onkeydown event, so that sets the timer which executes almost instantly, triggering our special event handler. In Safari 4, the textInput event fires post-DOM modification, so that clears the timer and triggers the handler, giving seamless `oninput` compatibility to the developer.

Late binding

No, not that kind of late binding. In order to mitigate the lack of event bubbling for our Internet Explorer polyfill, I opted to have the plugin not bind the input events to the element right away if it wasn’t an <input> or <textarea>. Instead, it binds several bubbling events — onfocusin, onmouseover and ondragstart — to the element. When one of those events fires, event.target.tagName is checked and if it is “INPUT” or “TEXTAREA” then we bind our input events to it. This allows delegate() to work really well in IE and I suspect, but haven’t verified, this is how jQuery is able to offer event delegation for non-propagating events such as onresize.

Problems with the new approach

Undoubtedly, the biggest problem is naming the special event. I can’t think of a way to have a special event named `input` that binds to the native `input` event and completely overrides it because, when we come to trigger the event later, we can’t. In my oh-so finite wisdom, I decided that the best solution I could provide was a differently named special event. So, when using the plugin now, you need to bind to "txtinput". $(selector).input(function () {}); will still work, though.

Another problem was teardown. Suppose we have a DOM structure like body -> div -> input — now, what happens if we bind to the <body> and <div> elements? Actually, this works completely as expected but, what if we unbind from one of those elements? Our teardown function needs to make sure that no other handlers are relying on our bound events before we remove them. The solution makes use of data and, every time an event is bound, increases an internal count to keep track. This counter is decreased for each of those elements in the teardown.

You can see the updated plugin at the original project page.

 

Utilizing the awesome power of jQuery to access properties of an element

28 Oct

I’m about 35% of the way through a 100gb backup at the moment, so I thought I’d throw in a quick blog post. I’m sure this has been brought up a million (or so) times but, in my opinion, you can’t reiterate this point enough. I occasionally get my hands dirty in the jQuery tags on Stack Overflow, and all too often I see code like this:

$(".myClass").click(function () {
    if ($(this).attr("id") == "myId") {
        runMyFunction($(this).val());
    }
    else if ($(this).attr("id") == "myOtherId")) {
        runMyOtherFunction($(this).val());
    }
});

The worst part is that this type of code is commonly given as an answer to a question. While I often feel tempted to vote down these solutions, they’re not incorrect per se, so I generally tend to post my own answer. “Why is this bad?”, you might be thinking. Well, lets simplify and break down the following code:

    if ($(this).attr("id") == "myId") {

We can separate $(this).attr("id") into 2 expressions; $(this) and attr. When you pass an argument to jQuery, it runs through the following process:

  1. Create a new instance of jQuery.fn.init(), passing this to the constructor which,
  2. initializes 4 variables,
  3. checks that the 1st argument passed isn’t a “falsy” value like null, undefined, “”, etc,
  4. looks for a nodeType property on the 1st argument passed, which is true in our case and,
  5. sets the resulting jQuery object’s context, 0 and length properties before returning the newly created instance.

Now .attr("id"):

  1. Pass the context (the collection of elements – only 1 in our case), our attribute name (“id”), value (undefined in this case), another argument named pass (true) and the jQuery.attr function to jQuery.access which,
  2. stores the length of the collection passed in as an optimisation,
  3. checks to see if the second argument is an object (a collection of property and value pairs to set on each element in the collection),
  4. checks to see if a property is being set, in our case it isn’t so it instead,
  5. checks the stored length of the collection isn’t 0 and calls the passed in jQuery.attr, passing the collection’s first element and the attribute name we tried to access. This function then,
  6. checks that the element argument passed in isn’t a “falsy” value, its nodeType property isn’t 3 or 8 (text and comment nodes respectively),
  7. checks that the pass argument was passed in (it wasn’t in this case),
  8. sets a variable that identifies whether the element passed in is an XML element (it’s not) and another that checks whether a value has been passed to assign to the property,
  9. tries to normalize attribute names into property names by looking it up in a map keeping the original name if it isn’t found,
  10. checks for a nodeType of 1 (element node), which is true in our case so then it,
  11. performs a regular expression test on the string to see if it matches /^(?:href|src|style)$/ and stores the result in a variable,
  12. checks to see if the string is “selected” to work around a bug in Safari. It isn’t, so it moves on and,
  13. checks to see if the string is the name of a property on the element, which is true in our case so it,
  14. checks to see if the set variable is “truthy”, it’s not so it skips that block and,
  15. checks to see if the element is a form element, it’s not so it,
  16. checks to see if the string is “tabIndex” to apply another workaround if true. It’s not so, finally, it feels justified to,
  17. return element[attribute].

Wow. Talk about taking the scenic route, we went through those 17 steps for jQuery to finally get to what we could have done in the first place: this.id. Of course, I’m not trying to put a downer on jQuery (it’s great and does all things!). jQuery *has* to do all this stuff for you so that you don’t run into the usual unexpected issues caused by certain implementations of getAttribute/setAttribute. Knowing which attributes are safe to access as property names means that you don’t need jQuery to do this leg work for you.

The real point of this post is that, when answering questions or helping people out with source code, try and set a good example. For the majority of attribute names you don’t need to use jQuery’s attr() to get the value of an attribute. If you’re unsure, look it up and find out if there are any specific quirks you need to know about. It might even be more efficient to work around those quirks without jQuery’s help.

I’ll leave you with an example that shows the difference over many iterations:

[iframe http://jsfiddle.net/AndyE/YPUTp/embedded/result,js height=100px width=500px]

 

Cross browser oninput event for jQuery

18 Oct

After posting my first technical blog post, Effectively detecting user input in JavaScript, I felt like I needed to expand on a few things that I didn’t cover. For instance, the fact that input is a bubbling event and propertychange is not, or that detecting support for input is made extra difficult by Firefox. Instead of expanding that article, I decided I would write a snippet to try and normalize the event a little better. I chose jQuery as a base for the code, mostly because I wanted to learn more about writing custom events for jQuery, but partly because I knew it would make a lot of things easier. The result is the input event plug-in below.

I wasn’t really sure where to start, but Googling turned up Ben Alman’s excellent jQuery special events post which tells you more or less everything you need to know. After that, I was able to create a basic plug-in that would provide an interface for jQuery.fn.bind("input"). The plug-in decides between attaching to input, propertychange or a host of fall back events that may or may not be supported in the browser. In short, it tries to get as much of the functionality of input as possible, but there’ll always be at least some functionality, for instance, plain keyboard input detection.

Note that the plug-in does have the following limitations in browsers that don’t support input:

  • There’s no “catch-all-changes” fallback. I had considered running a timer, when an element is focused or moused over, that constantly checks for value changes. However, this would trigger when the value of the element is changed programmatically, which may not be desired.
  • Because IE’s propertychange triggers when the value of an element is changed programmatically, that browser suffers from the problem described by the limitation above this one. It is possible that an infinite loop could be caused by always changing the value of the element in the event handler. A workaround, if you need to do this, might be to temporarily unbind whilst you change the value, rebinding after changing.
  • It currently only works with input and textarea elements. I may be able to get some support in for contentEditable elements in the future, but they don’t fire a propertychange event in IE when typing or pasting, which makes them awkward for capturing input. Firefox, Opera and IE9 don’t support the event on contentEditable elements anyway.
  • It will not work with live() or delegate(). This is due to how the propertychange event doesn’t bubble. Event delegation works as of version 1.1
  • It doesn’t work around the bugs Opera has with the input event. I’m not sure which versions are affected and it would be a pain to figure it all out. The plugin should now provide the best level of compatibility possible in all versions of Opera.

Changelog

  • v1.1: Oct 24, 2011 — Fixed many browser implementation issues, introduced live/delegate compatibility, changed event special name to txtinput. see full post.
  • v1.0: Oct 18, 2010 — 1st version

The following fiddle (very basically) demonstrates the event:

[iframe http://jsfiddle.net/AndyE/sdkBs/embedded/result 99% 300px]

And here’s the full code.

/* 
    jQuery `input` special event v1.1

http://whattheheadsaid.com/projects/input-special-event

    (c) 2010-2011 Andy Earnshaw
    MIT license
    www.opensource.org/licenses/mit-license.php
*/
(function($, udf) {
    var ns = ".inputEvent ",
        // A bunch of data strings that we use regularly
        dataBnd = "bound.inputEvent",
        dataVal = "value.inputEvent",
        dataDlg = "delegated.inputEvent",
        // Set up our list of events
        bindTo = [
            "input", "textInput", "propertychange", "paste", "cut", "keydown", "drop",
        ""].join(ns),
        // Events required for delegate, mostly for IE support
        dlgtTo = [ "focusin", "mouseover", "dragstart", "" ].join(ns),
        // Elements supporting text input, not including contentEditable
        supported = {TEXTAREA:udf, INPUT:udf},
        // Events that fire before input value is updated
        delay = { paste:udf, cut:udf, keydown:udf, drop:udf, textInput:udf };

    $.event.special.txtinput = {
        setup: function(data, namespaces, handler) {
            var triggerTimer,
                bndCount,
                changeTimer,
                // Get references to the element
                elem  = this,
                $elem = $(this),
                triggered = false;
            
            if (elem.tagName in supported) {
                bndCount = $.data(elem, dataBnd) || 0;

                if (!bndCount) 
                    $elem.bind(bindTo, handler);

                $.data(elem, dataBnd, ++bndCount);
                $.data(elem, dataVal, elem.value);
            } else {
                $elem.bind(dlgtTo, function (e) {
                    var target = e.target;
                    if (target.tagName in supported && !$.data(elem, dataDlg)) {
                        bndCount = $.data(target, dataBnd) || 0;
        
                        if (!bndCount) 
                            target.bind(bindTo, handler);

                        // make sure we increase the count only once for each bound ancestor
                        $.data(elem, dataDlg, true);
                        $.data(target, dataBnd, ++bndCount);
                        $.data(target, dataVal, target.value);
                    }
                });
            }
            function handler (e) {
                var elem = e.target;

                // Clear previous timers because we only need to know about 1 change
                window.clearTimeout(timer), timer = null;
                
                // Return if we've already triggered the event
                if (triggered)
                    return;

                // paste, cut, keydown and drop all fire before the value is updated
                if (e.type in delay && !timer) {
                    // ...so we need to delay them until after the event has fired
                    timer = window.setTimeout(function () {
                        if (elem.value !== $.data(elem, dataVal)) {
                            $(elem).trigger("txtinput");
                            $.data(elem, dataVal, elem.value);
                        }
                    }, 0);
                }
                else if (e.type == "propertychange") {
                    if (e.originalEvent.propertyName == "value") {
                        $(elem).trigger("txtinput");
                        $.data(elem, dataVal, elem.value);
                        triggered = true;
                        window.setTimeout(function () {
                            triggered = false;
                        }, 0);
                    }
                }
                else {
                    $(elem).trigger("txtinput");
                    $.data(elem, dataVal, elem.value);
                    triggered = true;
                    window.setTimeout(function () {
                        triggered = false;
                    }, 0);
                }
            }
        },
        teardown: function () {
            var elem = $(this);
            elem.unbind(dlgtTo);
            elem.find("input, textarea").andSelf().each(function () {
                bndCount = $.data(this, dataBnd, ($.data(this, dataBnd) || 1)-1);

                if (!bndCount)
                    elem.unbind(bindTo);
            });
        }
    };

    // Setup our jQuery shorthand method
    $.fn.input = function (handler) {
        return handler ? this.bind("txtinput", handler) : this.trigger("txtinput");
    }
})(jQuery);
​

Thanks to: Nick Craver, Tim Down, Ben Alman and Daniel Friesen. Please post any issues you come across in the comments.