Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy.on #127

Open
briancavalier opened this issue Jul 17, 2013 · 12 comments
Open

Proxy.on #127

briancavalier opened this issue Jul 17, 2013 · 12 comments

Comments

@briancavalier
Copy link
Member

See #108. Opening this specifically to discuss implementing Proxy.on as an API to allow a proxy to implement event handling that is specific to the type of component it is proxying. There are many custom event systems, and this would help wire adapt to them while also providing a consistent interface for developers via wire/on.

@skiadas
Copy link
Contributor

skiadas commented Feb 22, 2014

Let me see if I can get discussion going again on this topic, as I want to implement it in relation to d3.js. Briefly what happens in d3.js is you create a "selection", with something like d3.selectAll("svg"), and this returns a d3 object that is in effect supposed to be a d3 representative for the svg nodes in the dom. I am envisioning in my incorporation of d3 into wire that these selections would become wire components, and I would want to be able to have an on facet on them. That would translate to an on method that those d3 selection objects have, that functions similarly but slightly differently to the corresponding on handler placed on the dom node itself. Namely (https://github.com/mbostock/d3/wiki/Selections#wiki-on) it takes an event type with the usual allowances for css selectors, and a handler to be called. A couple of things it does differently however:

  1. It allows you to write something like: "click.foo" or "click.bar" for the event type to so to speak namespace handlers. Those would both bind to the click event, but they would not overwrite each other. By default d3 does not seem to allow multiple handlers on the same event, this is its way of doing it. I suspect this is something we should/might hide from the wire user.
  2. Before the handler is executed, d3 sets a property of the "global-ish" d3 object, namely d3.event, to refer to the event. This property is then used by other methods, in particular the handy d3.mouse(el) method that returns the coordinates of the mouse pointer relative to an element and in whatever coordinate system you have told d3 to define on that element. This is key functionality that I'd rather not give up on, hence the need for a plugin.
  3. The handler/listener is passed distinctly different information than normal event handlers. Namely the first parameter is the "data" associated with the target of the event, and the second is the index of the target in the selection array, while "this" is set to the dom element that is the target. The event itself is meant to be accessed via d3.event instead. So I think some translation will be in order to make this place nice with other events. I suspect this would be exactly the role of the overwritten proxy.on from the d3 plugin.

A bit longwinded discussion of how d3 works, but I wanted to have it down somewhere. I'll start a new comment with questions/suggestions.

@skiadas
Copy link
Contributor

skiadas commented Feb 22, 2014

So I think one first important question to answer is: Should this only work with dom events, but allow other components to "act" as dom elements if they are supposed to be managing one or more dom elements (backbone views, d3 selections etc). This would restrict the "event selectors" to the standard dom event and css selectors.
Or should it allow the creation of and listening to arbitrary events? If so, how would those events be triggered? It seems to me for this we would indeed need a proxy.trigger method.
But perhaps we could approach this in two steps, starting with proxy.on and dom events.

So how would the API look like? I think expecting the signature of proxy.on to be proxy.on(eventSelector, handler) would be the most reasonable, unless we want a third optional parameter for what "this" should be set to when the handler is called.

For event selectors I think either "eventName cssSelectors" or "eventName:cssSelectors" both seem reasonable forms.
For handlers, we have two questions: What arguments would it be passed, and what would "this" be set to? For arguments, the event being the first argument feels almost mandatory. I think most systems would pass the element that triggered the event as the second argument? I feel that perhaps "this" should be set to the component that the "on" facet was on, or its proxy. Not sure I have very clear use cases on these.

Just thinking out loud to get the discussion going.

So technically what would need to change? The way I envision it, I think, is that the "wire/on" facet would:

  1. resolve the wire "reference" to the function that is to be the event handler.
  2. Maybe do some sanitization check on the event type string.
  3. Call the proxy's "on" method.

Then the base plugin would essentially have an implementation that says "you can't do this", at least if we want to restrict to only dom-related events. And dom-related plugins would need to implement an "on" method in their proxies, starting with wire's dom object proxy, then backbone views, d3 etc.

Is that roughly what you had in mind?

@briancavalier
Copy link
Member Author

Hey @skiadas, sorry I've been kinda buried this week. Let me get my head back into this, and I'll respond soon. I did some preliminary work a while back to lay the groundwork for this, so I'll take a look at that to see where that stands, and let you know.

The goal would definitely be to allow more than just dom events. A proxy should be able to provide a simple on style interface to whatever event system of the thing it is proxying, within reason, of course. Since proxies can be specific to particular types (via Proxy.extend and wire plugins), customized proxies can do whatever is necessary to adapt the underlying component's event API.

I'll try to answer some of your specific questions soon, and give a summary of where things stand.

@skiadas
Copy link
Contributor

skiadas commented Feb 28, 2014

No worries @briancavalier, I see you guys have been busy, and I've been quite preoccupied with other things myself. More generic event handling sounds good, we'll need to figure out what the analog of CSS selectors would be for that, if any. Looking forward to your ideas.

@briancavalier
Copy link
Member Author

Ok! finally found some time to look back at where I left this, and just pushed the proxy-on branch, which refactors wire/on into a general purpose "on" facet that simply delegates to any component's proxy.on. Dom plugins (like wire/dom and wire/jquery/dom) implement a Proxy for DOM nodes that provides an appropriately implemented Proxy.prototype.on method.

Hopefully the proxy-on branch will be a good basis for discussion.

In that branch, wire/on considers the "event" parameters to be opaque--it can be anything (string, object, etc.) and will simply be passed through to the proxy's on() method. That means that the onus is on the developer to supply correct information for the particular type of component with which they're using "on". For dom nodes, that's an event type and selector pair, as it was before, eg: "click:.my-button", etc.

I can't see any other way to make this work in the general case right now. The wire/on plugin just has to pass through whatever information it is given, and the particular, component-specific proxy has to interpret and translate that information in some implementation-specific way--ie whatever it needs to do in order to hook up an event on the component. Consequently, Proxies should throw if they receive some event specifier that they can't make sense of, in order to tell the developer he/she made a mistake.

@briancavalier
Copy link
Member Author

I forgot to mention that the refactoring somewhat (but not completely) breaks the ability to create a partially applied on function using the on! resolver (eg on!click). The essence of the problem is that a proxy cannot be gotten synchronously. The means that the function created (eg by on!click) cannot attach an event handler in the current tick. In most cases, you'll never notice. But it does create some head scratcher cases, like this:

// Say this._listenToClicks was created and injected using `on!click`
this._listenToClicks(aDomNode, function(e) {
    console.log('clicked!');
});

aDomNode.click(); // Nothing happens

setTimeout(function() {
    aDomNode.click(); // Logs "clicked!"
}, 10);

We'll def need to think carefully about whether that is worth the tradeoff. Maybe there is a way to fix it, but it's certainly not obvious to me right now ... maybe @unscriptable will have some ideas.

@skiadas
Copy link
Contributor

skiadas commented Mar 17, 2014

This looks very promising, but there's one too many moving pieces right now for me. Which files should I be focusing on to understand what is going on? I'm seeing:

wire/on
lib/dom/on
lib/plugin-base/on

For starters, and I'm not entirely sure how it all fits in together. It seems to me that "lib/plugin-base/on" implements a generic "on-plugin generator", that is meant to take a custom event-handling function and turn it into a wire plugin thing?
Would I be trying to call this from my d3 plugin?
I was trying to look for a sample, so looked in dojo/on, but that seems to be trying to load itself?
Maybe I should be looking at jquery for an example?

@briancavalier
Copy link
Member Author

@skiadas Yes, sorry, the branch is still a bit messy, and several of the other "on"-related things can likely just be removed. This new proxy-based model kind of makes them obsolete. I'll try to make some time today to clean up the branch.

First, a question:

What are the d3 things that you want to connect to? Are they DOM Nodes? Are they d3-specific JS objects? If they are DOM Nodes, then you may not need to do any work at all. Read on, though :)

Ok, on to your specific questions:

It seems to me that "lib/plugin-base/on" implements a generic "on-plugin generator", that is meant to take a custom event-handling function and turn it into a wire plugin thing?

Basically, yes. In the master and dev branches, that's exactly what lib/plugin-base/on does. Before Proxy.prototype.on, it was a helper that made it easier to create an "on" plugin for DOM events. In the proxy-on branch, it is obsolete and I need to remove it. The base DOM functionality in the proxy-on branch is implemented by the base DOM Node proxy in lib/dom/NodeProxy. That proxy is then extended with DOM Event capabilities (by standard JS inheritance) in wire/dom (which leans on lib/plugin-base/dom, a "plugin generator" as you described--more on that below)

Would I be trying to call this from my d3 plugin?

In the old world, yes :) In the proxy-on world, no. In the proxy-on world, for custom d3 events, you will write a wire d3 plugin that provides a proxy for the kind of d3 components to which you would like to connect. By simply implementing the proxy, wire/on will instantly gain the ability to connect to d3 components when you include your wire d3 plugin in a spec.

I was trying to look for a sample, so looked in dojo/on, but that seems to be trying to load itself?

Sorry, wire/dojo/on is obsolete in the proxy-on world. Instead, the wire/dojo/dom plugin now provides a DOM Node proxy which inherits from the base NodeProxy. It does this by leaning on lib/plugin-base/dom, which is similar to what you described above: a "dom-plugin generator" that helps someone write a wire plugin for a specific DOM library, such as jQuery or Dojo.

wire/dojo/on does not load itself :) It loads dojo/on, which is the on module from the dojo package, ie Dojo Toolkit's dojo/on module. If it did load itself, it would specify the module id ./on, but that would cause a rift in the universe :)

Maybe I should be looking at jquery for an example?

The best place to look right now is lib/plugin-base/dom. Remember that it is a "plugin generator", so while it's not exactly an analog for what you'll need to do, it does contain some interesting bits that you can use as a reference.

The first related thing it does is to create a new proxy by inheriting (via standard Object.create technique) from the basic NodeProxy, and implements a custom on method, and a helper function that "stacks" new functionality onto an existing proxy.

That last bit deserves just a bit of explanation. Internally, wire creates a base proxy for all components it creates. Plugins may then stack additional functionality onto that using the WireProxy.extend helper. Notice that it also first checks to see if the proxy's target is indeed a DOM Node before stacking new functionality onto it.

In your d3 plugin, depending on what your proxy will be proxying (I don't know d3 well enough to know), your d3 proxy will inherit from either NodeProxy (if you will be proxying and connecting to DOM Nodes), or from ObjectProxy if you will be proxying JS Objects.

The second related thing lib/plugin-base/dom does is to return a wire plugin instance that exposes a function that creates proxies.

To implement a d3 plugin that supports using "on" for event connections with d3-specific components

You'll write a wire plugin that:

  1. Creates a proxy descended from NodeProxy or ObjectProxy, and implements a custom on method for d3 events. The on method must have the signature: function(eventString, eventHandlerCallback), where eventString is whatever string was provided on the "left side" in a wire spec, eventHandlerCallback is a the function you should hook up to an event.
  2. Exposes a plugin-creating function with the signature function(baseProxy). This function should test baseProxy.target to ensure that it is something that your plugin should be proxying. If it is, use WireProxy.extend to extend it in a similar way as lib/plugin-base/dom does and return the extended proxy. If it's not then simply return baseProxy.

(See also the Proxy docs)

To use your new plugin

Once you have your plugin, you can use it in a wire spec:

d3ThingYouWantToConnect: {
    // Use "create" or however you create/get a d3 thing, then
    on: {
        // 'someEventString' can be whatever makes sense in a d3 context and will be
        // passed, verbatim, as the first parameter to your d3 proxy's
        // on(eventString, eventHandlerCallback) method. Wire/on will automatically
        // turn 'myOtherComponent.handleD3Event' into a function and pass it as the
        // second parameter to your d3 proxy's on(eventString, eventHandlerCallback) method
        'someD3EventString': 'myOtherComponent.handleD3Event'
    }
}
$plugins: ['wire/on', 'skiadas/d3WirePlugin']

Sorry to be long winded! Hopefully, some of that was helpful! Like I said, I'll try to clean up the branch a bit more to remove all the "on" noise.

@skiadas
Copy link
Contributor

skiadas commented May 26, 2014

Hi @briancavalier
thanks for this detailed explanation! The academic year is over and I can finally focus more on this and getting d3 smoothly integrated into wire, and I think the above gives me a starting point. But if you have a more cleaned up version of proxy-on, it would help.

Basically in d3 you would do something like say: d3.select(aCSSselector), which will look for a set of nodes matching the selector, and create a d3 object that "represents" that collection. A normal course of events would then be to call various d3 methods of that object, one of them being an "on" method, which will effectively register a handler to handle some event on the nodes of the selection. So it sounds to me that your scheme above is more or less what I would need to do. d3 handles those events in a somewhat different way, setting some global values and the "this" object, but I want it to play nice with non-d3 components, and that's mainly where the plugin would be coming in, translating the d3-event into a "normal" dom event that the eventHandlerCallback can understand. In essence, I'd like someone who tries to connect to the d3 object to be able to treat it just as if they were connecting to the DOM node directly.

Hm I think I am starting to ramble on, I just wanted to touch base on this and to see if there are any updates before I dig in.

@briancavalier
Copy link
Member Author

@skiadas Great! This sounds like it won't be too hard. It seems like we'll need some way to "create a d3 object that "represents" that collection" in wire. One possibility would be for this new d3 wire plugin to expose a factory or a resolver, in addition to the Proxy. In the case of a factory, this may work out quite well as factories are allowed to return a Proxy, which avoids the need to query all currently active plugins looking an appropriate proxy-creating function.

Either way, once a d3 object has been created and proxied, wire/on should "just work".

translating the d3-event into a "normal" dom event that the eventHandlerCallback can understand. In essence, I'd like someone who tries to connect to the d3 object to be able to treat it just as if they were connecting to the DOM node directly.

This sounds very interesting. Do you know how "d3 events" differ from "normal" DOM events?

I haven't made the time yet to clean up the proxy-on stuff, but I will try to make time over the next couple days.

@skiadas
Copy link
Contributor

skiadas commented May 27, 2014

A factory is exactly what I had in mind!

"d3 events" are described here:
https://github.com/mbostock/d3/wiki/Selections#on

Basically, it's not that the events are not regular DOM events, but the expected form of the handlers is completely different. The arguments passed to the handler, instead of the event, are the "datum" and "index" in the collection associated with the DOM element that triggered the event. So the handler that you would normally provide to aD3object.on would have the form function(d,i) {}. Also this handler is called with "this" set to the dom element that triggered the event. The handler is supposed to access the event by accessing the "event" property of the global d3 object, namely d3.event (https://github.com/mbostock/d3/wiki/Selections#d3_event). It is important that this property is set, as it enables a number of other d3 functionality.

So I am thinking my proxy object would need to take a handler with a signature of function(event,element){} or something of the sort, and create a "d3-handler" wrapper around it, then pass that wrapper to the aD3object.on method. Haven't fully worked out the details yet.

I'm thinking I could expose a "d3on" facet, distinct from "on", that allows handlers to be attached that use the d3-event form instead of the standard DOM-event form. This way d3-components would be able to place nicer with each other, while still being able to connect to non-d3 components. But that's another matter.

@skiadas
Copy link
Contributor

skiadas commented May 28, 2014

@briancavalier "In the case of a factory, this may work out quite well as factories are allowed to return a Proxy". Can you elaborate on this?

  1. Are you saying, that if the component passed to my factory method's provided resolver.resolve is an extension of baseProxy, then it would completely circumvent the process of testing the various proxies?
  2. How would I get hold of baseProxy in the factory method?
  3. Are there other advantages to doing it that way, instead of returning a d3.selection object from the factory, and have the specializeProxy method sniff out if the component is of the type that it's supposed to be extending?
  4. Would there still need to be a "proxies" property in my plugin, if my factory is returning proxies?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants