- From: Sean Hogan <shogun70@westnet.com.au>
- Date: Sat, 13 Dec 2008 00:16:21 +1100
- To: Lachlan Hunt <lachlan.hunt@lachy.id.au>
- CC: public-webapps@w3.org
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