0

I'm trying to get an Observable from a d3 "enter" selection. I can't find a way to do this properly.

For example, for the following selection:

selection
    .attr("class", "node")
    .attr('id', (d) => d.id)

Where selection is an enter() selection, i want to have the click event as an Observable. How can I do this?

I tried with fromEvent

const clickStream = Rx.Observable.fromEvent(selection[0], 'click')

Which seems like it should work because selection[0] is an array of DOM nodes (right?).

So how can I let this play nice together?

bryanph
  • 993
  • 7
  • 18
  • Isn't `selection[0]` just a *single* DOM node (the first of the list), not an array of DOM nodes? – noppa Jun 19 '16 at 11:26
  • Ah no, my mistake, tried it with d3 and you are correct. Instead, could you post how the `selection` is selected? – noppa Jun 19 '16 at 11:45

3 Answers3

4

From the documentation for Rx.Observable.fromEvent:

The DOMElement, NodeList, jQuery element, Zepto Element, Angular element, Ember.js element or EventEmitter to attach a listener.

The problem is that selector[0] that we get from d3 selection is none of those. It's just a regular Array containing the nodes, which confuses RxJS.

Solution 1

Instead of passing the d3 selection object to Rx.Observable.fromEvent, you could re-select the nodes with something that returns something RxJS can handle, like with jQuery or document.querySelectorAll('.node').

Solution 2

Alternatively, you can loop through the nodes in the array and pass them for RxJS one by one.

function d3EventObservable(selection, event) {
    //Start with an observable that will never emit
    var obs = Rx.Observable.never();
    selection.each(function() {
        //Create observables from each of the elements
        var events = Rx.Observable.fromEvent(this, event);
        //Merge the observables into one
        obs = obs.merge(events);
    });
    return obs;
}

const clickStream = d3EventObservable(selector, 'click');
noppa
  • 3,947
  • 21
  • 22
  • Solution 1 reselects all nodes all the time right? So in this way I can't use d3's enter/exit flow. Solution 2 is indeed what I want, however do you know if d3 uses one listener per entity for the enter selection, because it seems like this won't scale for thousands of nodes. (also sorry for my late reaction) – bryanph Jun 26 '16 at 08:15
  • @bryanph I find solution 2 better too. Not sure about the internals of d3, to be honest I don't even have that much experience with it. If you are worried about the perf, though, I recommend you just go ahead and create a test with thousands of nodes and see how well it works. – noppa Jun 26 '16 at 09:52
1

My Usecase: I am new to d3/rxjs, and want this functionality. Want to use d3.on only when I need to manipulate data from events, but for the entire control flow/ajax requests and stuff, would like Rxjs to come forward, and want to do this often, with as little boilerplate as possible.

MonkeyPatch: As noppa has already mentioned, Rx.Observable.fromEvent expects a NodeList, which is the standard output of any getElementBy*/querySelector*.

But d3.select/selectAll returns Array[Array]

The exact expectation is in this Rxjs code section. This only expects an [object NodeList], and as Arrays & NodeList already have .length in common, this will work for Rxjs fromEvent.

A monkeypatch is required here because gecko/v8 don't expose a method to make custom NodeList Objects.

Adding to noppa's solutions:

Solution 3

function returnNodeListFrom(selected){
  if(Array.isArray(selected)){
    newArray=selected;
    newArray.toString=function(){return '[object NodeList]'};
     return newArray; 
  }else return selected;
}

Then, you can do

Rx.Observable.fromEvent(returnNodeListFrom(selection[0]),'click').subscribe();

Solution 4

//Slightly better version of 3 using lodash
function returnNodeListFrom(selected){
  if(Array.isArray(selected)){
    newArray=_.flatten(selected);
    newArray.toString=function(){return '[object NodeList]'};
     return newArray; 
  }else return selected;
}

Then, you can do

Rx.Observable.fromEvent(returnNodeListFrom(selection),'click').subscribe();
  • Monkeypatch is actually not needed. If you feel that the best approach is to first convert the array to NodeList, you can use [document fragment](http://stackoverflow.com/questions/13351966/create-node-list-from-a-single-node-in-javascript) for that. IMO it just opens unneeded possibilities of perf problems and bugs, but it is one way to go nontheless. – noppa Jun 25 '16 at 21:20
  • Hey.. yes, looked into that too.. The issue with document fragment is that it detaches the nodes from selection - E.g. check the 3rd and 4th comments on the selected answer for that link - they suggest this method has side effects and all future access on these nodes will fail. Meaning, your exit/remove and enter/append will not be as clean as you'd want. Again, my usecase is to be able to switch between Rx and D3 - Rx for throttles/debounce etc and d3 for only data manipulation. – user6387024 Jun 26 '16 at 04:41
0

I ended up using part of noppa's suggestion. At the beginning of the enter selection I set an enter-selection flag as a class

selection
.attr("class", "node")
.classed('enter-selection', true) // for rxjs..

Then I select the corresponding enter selection using querySelectorAll:

const domNodes = document.querySelectorAll('.node.enter-selection')

Then I subscribe to the events that i need, for example:

const domNodes = document.querySelectorAll('.node.enter-selection')

And finally at the end of the enter selection I remove the class flag:

selection.classed('enter-selection', false)
bryanph
  • 993
  • 7
  • 18