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();

})()

Reply via email to