Re: [selectors-api] matchesSelector() Proposal

Lachlan Hunt wrote:
>
> I forgot to mention before that I already have an open bug for 
> tracking this issue.
>
> http://www.w3.org/Bugs/Public/show_bug.cgi?id=5865
Ta.
>>
>> Are you asking for a better explanation of the concepts or more 
>> specific examples?
> It would be useful if you could elaborate upon those use cases.
>
> Well, you already explained the concept of event delegation below, but 
> some examples would also be useful, especially real world examples.  
> Do any JavaScript libraries implement functionality similar to the 
> matchesSelector proposal?  If so, then examples of those would also be 
> useful.
Current implementations:

base2:
  element.matchesSelector(selector) // if the element is already enhanced
    or
  base2.DOM.Element.matchesSelector(element, selector)

  
http://base2.googlecode.com/svn/doc/base2.html#/doc/!base2.DOM.Element.matchesSelector

YUI:
  YAHOO.util.Selector.test( element, selector ) // element can also be a 
string in which case it is treated as the id of the element to be tested

  http://developer.yahoo.com/yui/docs/YAHOO.util.Selector.html

Prototype:
  element.match( selector ) // if the element is already enhanced
    or
  Element.match( element, selector )

  http://www.prototypejs.org/api/element/match

jQuery:
 $(element).is(simpleSelector) // if simpleSelector contains hierarchy 
selectors (such as +, ~, and >) will always return 'true'
 
 This method actually works on the current selection and "returns true, 
if at least one element of the selection fits the given expression."
 Sort of like a NodeList.matchesSelector() if there was such a thing.
 http://docs.jquery.com/Traversing/is

ExtJS:
  Ext.DOMQuery.is ( element, simpleSelector ) // element can also be the 
string-id of an element, or an array of elements.

  
http://extjs.com/deploy/dev/docs/output/Ext.DomQuery.html#Ext.DomQuery-methods


Probably others exist. I imagine they've come up in discussions on and 
off-list.
>
>>> How is that nonsensical?  Without having use cases presented, it's 
>>> hard to justify the feature and even harder to make sure it's 
>>> designed in the most optimal way for those use cases.
>>
>> - The feature is already justified by the first paragraph of the 
>> specification. It facilitates the performing of DOM operations on an 
>> element that matches a selector.
>
> There are other potential features that fit that description, but 
> which weren't included in this version.  For example, NodeList 
> filtering, scoped methods and namespace resolution, each of which will 
> be reconsidered for the next version.
>
As you have said, it is too late to add matchesSelector() to the current 
spec. Can I point out for the future that none of those features are the 
loss that matchesSelector() is.
- NodeList filtering would have a straight-forward alternative if we had 
matchesSelector()
- scoped queries often aren't necessary, and when they are they can be 
faked by generating an ID for the :scope element
- namespace resolution only cuts out a small proportion of web-pages, 
and can be worked around in a relatively straight-forward manner if 
necessary

>> - Event delegation plus Element.matchesSelector is a better match for 
>> event registration that querySelectorAll.
>> Say you want to add event handlers to elements that match a selector. 
>> If you perform document.querySelectorAll(S1) and then 
>> addEventListener on each found element, and then one such element 
>> (E1) is relocated in the document in such a way that it no longer 
>> matches S1 then (presumably) the handlers attached on E1 become 
>> invalid and need to be removed (and perhaps different ones added).
>
> Sure, if you're using event delegation, then I agree that a 
> matchesSelector method could be useful.  But be aware that event 
> delegation is just one possible solution to that problem.  XBL2 is 
> another more flexible solution with features designed specifically for 
> that.  Hopefully that will become available within a few years, but 
> unfortunately, implementing it isn't yet a priority for implementers.
>
I'm glad you mentioned XBL2. One could implement a light-weight XBL2 
with event-delegation and element.matchesSelector() in, say, 250 lines 
of javascript.
Of course, there are going to be limitations:
- won't support templates,
- no state is maintained in bindings. This may not be an issue if state 
is maintained on the element itself. e.g. aria attributes
- the public interfaces of bindings are not available. This could be 
mitigated by a getBinding() method on the element, similar to 
hasBinding() in the XBL specification. getBinding() would also require 
element.matchesSelector().
- xblEnteredDocument() and xblLeftDocument() methods are never called. 
This could possible be mitigated with DOM mutation events

So it wouldn't fulfil the component delivery aspect of XBL2. But it 
would fulfil the declarative, automatic binding aspect.

Anyway, here's the light-weight XBL2 implementation I wrote yesterday.
It just needs wrapper-classes for XBLDocument, etc. Plus 
element.matchesSelector() and probably a DOM Events compatibility layer 
and maybe a few other patches to bring various browsers up to standards.

Since I just happened to have those lying around you can check out some 
primitive demos at:
  http://www.meekostuff.net/XBLpm/demos/index.html
They aren't fancy, they just demonstrate that it works - tested on 
Firefox-3, Safari-3, Opera-9.6.

And it should work no matter how you manipulate the DOM (though I 
haven't got a demo for that).
See the article by the author of reglib illustrating the resilience and 
simplicity of event-delegation compared to load-traverse-modify.
  http://blogs.sun.com/greimer/entry/reglib_versus_jquery

None of this is to say that event-delegation and matchesSelector() are 
silver-bullets, or don't have their own weaknesses. They are just tools 
that are sometimes the right ones for the job.

Sean

PS I don't expect you to read this code. Just pointing out that I'm not 
just full of hot air.


/*
 Light-weight (or poor man's) XBL2.
 (c) Sean Hogan, December 2008
 All rights reserved.

 NOTES:
 Uses event delegation and late-binding to do everything.
 Bindings are created when an event is handled and destroyed immediately 
after.
 Therefore the bindings are stateless.
 Doesn't support xbl:template.
 xblBindingAttached, etc are never called.
*/

(function() {

/*
 init() kicks everything off. It gets called at the end of the script
*/
function init() {
    registerXBLProcessingInstructions();
    registerXBLLinkElements();
    registerXBLStyleElements();
    configureEventDelegation();
}

function registerXBLProcessingInstructions() {
    for (var node=document.firstChild; node; node=node.nextSibling) {
        if (node == document.documentElement) break;
        if (node.nodeType != Node.PROCESSING_INSTRUCTION_NODE) continue;
        if ("xbl" != node.target) continue;
        var m = node.data.match(/^\s*href=['"]([^'"]*)['"]/);
        loadBindingDocument(m[1]);
    }
}

function registerXBLLinkElements() {
    var head = document.getElementsByTagName("head")[0];
    for (var node=head.firstChild; node; node=node.nextSibling) {
        if (node.nodeType != Node.ELEMENT_NODE) continue;
        if (node.tagName.toLowerCase() != "link") continue;
        if (node.rel != "bindings") continue;
        loadBindingDocument(node.href);
    }
}

function registerXBLStyleElements() {
    var head = document.getElementsByTagName("head")[0];
    for (var node=head.firstChild; node; node=node.nextSibling) {
        if (node.nodeType != Node.ELEMENT_NODE) continue;
        if (node.tagName.toLowerCase() != "style") continue;
        if (node.type != "application/xml") continue;
        var text = node.textContent || node.innerHTML; // TODO standardize??
        loadBindingDocumentFromData(text, document.URL);
    }
}

var bindingDocuments = []; // NOTE these are XBLDocument wrappers around 
the actual XBL documents
var xblDocuments = {};

function loadBindingDocument(uri) {
    var xblDoc = importXBLDocument(uri);
    if (!xblDoc || !xblDoc.bindings) {
        logger.error("Failure loading binding document " + uri);
        return;
    }
    bindingDocuments.push(xblDoc); // WARN assumes loadBindingDocument 
never called twice with same uri
    importDependencies(xblDoc);
}

function loadBindingDocumentFromData(data, uri) {
    var xml = (new DOMParser).parseFromString(data, "application/xml"); 
// TODO catch errors
    var xblDoc = XBLDocument(xml, uri);
    if (!xblDoc || !xblDoc.bindings) {
        logger.error("Failure loading binding document from data");
        return;
    }
    bindingDocuments.push(xblDoc);
    importDependencies(xblDoc);
}

function importXBLDocument(uri) {
    var absoluteURI = resolveURL(uri, document.URL);
   
    // check the cache
    var xblDoc = xblDocuments[absoluteURI];
    if (typeof xblDoc != "undefined") return xblDoc;
   
    // otherwise fetch and wrap
    var rq = new XMLHttpRequest();
    rq.open("GET", absoluteURI, false);
    rq.send("");
    if (rq.status == 200) {
        var xblDoc = XBLDocument(rq.responseXML, uri);
        xblDocuments[absoluteURI] = xblDoc;
    }
    else {
        xblDocuments[absoluteURI] = null; // NOTE placeholder
        logger.error("Failure loading xbl document " + uri);
    }
    return xblDoc;
}

function importDependencies(xblDoc) {
    for (var i=0, binding; binding=xblDoc.bindings[i]; i++) {
        if (!binding.element) continue;
        importBaseBinding(binding);
    }
}

function importBaseBinding(binding) {
    if (!binding.baseBindingURI) return;
    if (typeof binding.baseBinding != "undefined") return;
    var m = binding.baseBindingURI.match(/^(.*)#(.*)$/); // FIXME 
bindingURI need not have #id
    var xblDoc;
    if (m[1] == "") xblDoc = binding.xblDocument;
    else {
        var absoluteURI = resolveURL(m[1], binding.xblDocument.documentURI);
        xblDoc = importXBLDocument(absoluteURI);
    }
    var baseBinding = xblDoc.namedBindings[m[2]];
    if (baseBinding) {
        binding.baseBinding = baseBinding;
        importBaseBinding(baseBinding);
    }
    else binding.baseBinding = null; // place-holder
}

/*
 configureEventDelegation() makes a lookup-table of handlers by looping 
over:
   valid handlers of bindings with element-selectors in every 
binding-document
*/
var handlerTable = {}; // NOTE accessed with 
handlerTable[String:eventType][Number:eventPhase][Number:handlerIndex]

function configureEventDelegation() {
    for (var i=0, xblDoc; xblDoc=bindingDocuments[i]; i++) {
        for (var j=0, binding; binding=xblDoc.bindings[j]; j++) {
            if (!binding.element) continue; // NOTE bindings without an 
element-selector never apply
            registerBinding(binding);
        }
    }
}

function registerBinding(binding) { // FIXME doesn't break inheritance loops
    if (binding.baseBinding) registerBinding(binding.baseBinding); // 
FIXME doesn't facilitate calling baseBinding
    for (var k=0, handler; handler=binding.handlers[k]; k++) {
        var type = handler.event;
        if (!type) continue; // NOTE handlers without type are invalid
        var phase = handler.phase;
        if (!handlerTable[type]) { // i.e. first registration for event.type
            document.addEventListener(type, dispatchEvent, true); // 
route through our event-system
            handlerTable[type] = new Array(4);                     // 
and pre-allocate space in handlerTable
            handlerTable[type][1] = []; // capture
            handlerTable[type][2] = []; // target
            handlerTable[type][3] = []; // bubbling
        }

        var handlerRef = { binding: binding, handler: handler };
        if (phase) handlerTable[type][phase].push(handlerRef);
        else { // no specified phase means AT_TARGET or BUBBLING_PHASE
            handlerTable[type][2].push(handlerRef);
            handlerTable[type][3].push(handlerRef);
        }
    }
}

/*
 dispatchEvent() takes over the browser's event dispatch. It is designed 
to be attached as a listener on document.
 It determines the event-path and routes the event through capture, 
target and bubbling phases.
 For each element on the path it determines if there are valid handlers, 
and if so
 it creates the associated binding and calls the handler.
*/
function dispatchEvent(event) {
    event.stopPropagation(); // NOTE stopped because we handle all 
events here
   
    var phase = 0,
        target = event.target,
        current = target,
        path = [];
       
    // precalculate the event-path thru the DOM
    for (current=target; current!=document; current=current.parentNode) 
path.push(current);
   
    /*
      callHandlers() is a pseudo event-listener on currentTarget. It is 
called on every element in the event-path.
      It finds appropriate xbl-handlers by matching event type and 
phase, and current.matchesSelector().
      Valid handlers are called with 'this' set to a new instance of the 
binding implementation.
      i.e. no state is saved in bindings
    */
    function callHandlers() {
        Meeko.stuff.domSystem.attach(current); // NOTE add standard DOM 
methods. Just classList on most platforms.
        var handlerRefs = handlerTable[event.type][phase];
        for (var i=0, handlerRef; handlerRef=handlerRefs[i]; i++) {
            var binding = handlerRef.binding;
            var handler = handlerRef.handler;
            if (binding.element && 
!current.matchesSelector(binding.element)) continue; // NOTE no 
element-selector means this is a base-binding
            if (!handler.matchesEvent(event, { eventPhase: false })) 
continue; // NOTE switch off eventPhase checking

            // instantiate internal object
            var internal = new binding.implementation;
            internal.boundElement = current;

            // instantiate internal object for baseBindings
            // FIXME this is inefficient if more than one binding in a 
chain will handle the same event
            // as the binding chain gets built up every time.
            var b0 = binding, i0 = internal;
            do {
                var b1 = b0.baseBinding;
                if (!b1) break;
                var i1 = new b1.implementation;
                i1.boundElement = current;
                i0.baseBinding = i1;
                b0 = b1; i0 = i1;
            } while (b0); // NOTE redundant
            // execute handler code
            if (handler.action) try { // NOTE handlers don't need an action
                handler.action.call(internal, event);
            }
            catch(error) { logger.debug(error); } // FIXME log error
           
            if (handler.defaultPrevented) event.__preventDefault();
            if (handler.propagationStopped) event.__stopPropagation();
        }
    }

    // override event properties and methods
    event.__defineGetter__("currentTarget" , function() { return 
current; }); // WARN not working for Safari
    event.__defineGetter__("eventPhase" , function() { return phase; }); 
// WARN not working for Safari
    event.eventStatus = 0;
    event.__preventDefault = event.preventDefault;
    event.preventDefault = function() { this.eventStatus |= 1; };
    event.__stopPropagation = event.stopPropagation;
    event.stopPropagation = function() { this.eventStatus |= 2; };

   
    phase = Event.CAPTURING_PHASE;
    for (var n=path.length, i=n-1; i>0; i--) {
        callHandlers();
        if (event.eventStatus & 1) event.__preventDefault();
        if (event.eventStatus & 2) return;
    }

    phase = Event.AT_TARGET;
    current = path[0];
    callHandlers();
    if (event.eventStatus & 1) event.__preventDefault();
    if (event.eventStatus & 2) return;
    if (!event.bubbles) return;

    phase = Event.BUBBLING_PHASE;
    for (var n=path.length, i=1; i<n; i++) {
        current = path[i];
        callHandlers();
        if (event.eventStatus & 1) event.__preventDefault();
        if (event.eventStatus & 2) return;
    }
   
    return;
}

init();

})()

Received on Friday, 12 December 2008 13:17:12 UTC