neezer
neezer

Reputation: 20580

How to chain subscriptions on Rx.Observabale? (refactor)

If I have an Rx.Observable, how can I subscribe multiple functions to it via forEach? The code below works, but this part in particular feels very un-DRY to me:

Rx.Observable.from(definition).forEach(highlight);                   
Rx.Observable.from(definition).forEach(prefix);

I know I could create a wrapper function that invokes these two within it, but I like the readability of keeping them separate. What I'd love is to do something akin to

Rx.Observable.from(definition).forEach(highlight, prefix)

or

definition = Rx.Observable.from(definition)
definition.forEach(highlight)
definition.forEach(prefix)

or

Rx.Observable.from(definition).forEach(highlight).forEach(prefix)

... but none of those work. What would be the best way to refactor this?


JS:

(function commands() {                                                   
  function highlight(node) {                                               
    node.classList.add(hoverClass);                                        
  }                                                                        

  function unhighlight(node) {                                             
    node.classList.remove(hoverClass);                                     
  }                                                                        

  function prefix(node) {                                                  
    node.classList.add(prefixClass);                                       
  }                                                                        

  function unprefix(node) {                                                
    node.classList.remove(prefixClass);                                    
  }                                                                        

  function unprefixAll(nodes) {                                            
    Rx.Observable.from(nodes).forEach(unprefix);                           
  }                                                                        

  var hoverClass  = "hover";                                               
  var prefixClass = "prefixed";                                            
  var $commands  = document.querySelector("#commands");                
  var definitions = Rx.Observable.from($commands.querySelectorAll("dt"))  
    .map(function(_, i) {                                                  
      return $commands.querySelectorAll(                                  
        "dt:nth-of-type("+ (i + 1) +"), dt:nth-of-type("+ (i + 1) +") + dd"
      );                                                                   
    });                                                                    

  definitions.forEach(function (definition) {                              
    Rx.Observable.fromEvent(definition, "mouseover").forEach(function() {  
      definitions.forEach(unprefixAll);                                    
      Rx.Observable.from(definition).forEach(highlight);                   
      Rx.Observable.from(definition).forEach(prefix);                      
    });                                                                    

    Rx.Observable.fromEvent(definition, "mouseout").forEach(function() {   
      Rx.Observable.from(definition).forEach(unhighlight);                 
    });                                                                    
  });                                                                      
})();  

HTML:

<dl id="commands">                                                                          
  <dt class="prefixed">command 1</dt>                                                          
  <dd>does a thing for command 1</dd>   
  <dt>command 2</dt>                                                          
  <dd>does a thing for command 2</dd>    
  <dt>command 3</dt>                                                          
  <dd>does a thing for command 3</dd>    
  <dt>command 4</dt>                                                          
  <dd>does a thing for command 4</dd>                                                                            
  <dt>help</dt>                                                                                
  <dd>Shows all available commands</dd>                                                        
</dl>

Upvotes: 4

Views: 1830

Answers (1)

paulpdaniels
paulpdaniels

Reputation: 18663

You should remember that one of the real powers of RxJS is in operator composition.

Rather than trying to forEach()/subscribe() through everything (which you can do with traditional javascript arrays without needing to include Rx), you should try to think about how events are transformed and manipulated as they travel down a pipeline.

The following is just one example of how you could do it through a single pipeline:

  //Gets a subscription which can be used to clean up all the internal streams
  //Use flatMap to flatten the inner streams into a single stream
  var subscription = definitions.flatMap(function (d) {       
      var mouseOver = Rx.Observable.fromEvent(d, "mouseover");
      var mouseOut = Rx.Observable.fromEvent(d, "mouseout");
      var definition = Rx.Observable.from(d);

      //Merge together both mouseOver and mouseOut so we can cancel them together later
      //Use tap to apply side effects.
      return Rx.Observable.merge(mouseOver.flatMap(definition)
                                          .tap(prefix)
                                          .tap(highlight),
                                 mouseOut.flatMap(definition)
                                         .tap(unprefix)
                                         .tap(unhighlight));

 }).subscribe();

Edit 1

To elaborate on what is going on here:

  1. Use flatMap to listen to the definitions stream, each value d will be an matched element to manipulate.
  2. We then create the listeners for the two events as well as one to iterate through the matched elements.
  3. Next we use flatMap again to capture each event (mouseOver and mouseOut) and project instead the observable for our matched elements.
  4. Next we apply side effects through the use of tap.
  5. Merge together both of these streams into a single stream, this is primarily so that the subscriptions get passed back up to the top level.
  6. Finally subscribe to the whole chain, then when you dispose of the returned Disposable it will clean up all the inner streams as well.

Here is the full working example:

(function commands() {                                                   
  function highlight(node) {                                               
    node.classList.add(hoverClass);                                        
  }                                                                        

  function unhighlight(node) {                                             
    node.classList.remove(hoverClass);                                     
  }                                                                        

  function prefix(node) {                                                  
    node.classList.add(prefixClass);                                       
  }                                                                        

  function unprefix(node) {                                                
    node.classList.remove(prefixClass);                                    
  }                                                                                                                                    
    
  var hoverClass  = "hover";                                               
  var prefixClass = "prefixed";                                            
  var $commands  = document.querySelector("#commands");                
  var definitions = Rx.Observable.from($commands.querySelectorAll("dt"))  
    .map(function(_, i) {                                                  
      return $commands.querySelectorAll(                                  
        "dt:nth-of-type("+ (i + 1) +"), dt:nth-of-type("+ (i + 1) +") + dd"
      );                                                                   
    });
        
  var subscription = definitions.flatMap(function(d) {
      //Declare this stuff up front        
      var mouseOver = Rx.Observable.fromEvent(d, "mouseover");
      var mouseOut = Rx.Observable.fromEvent(d, "mouseout");
      var definition = Rx.Observable.from(d);
            
      return Rx.Observable.merge(mouseOver.flatMap(definition).tap(prefix).tap(highlight),
                                 mouseOut.flatMap(definition).tap(unprefix).tap(unhighlight)).ignoreElements();   
    }).subscribe();
    
    
    
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/2.5.3/rx.all.js"></script>
<dl id="commands">                                                                          
  <dt class="prefixed">command 1</dt>                                                          
  <dd>does a thing for command 1</dd>   
  <dt>command 2</dt>                                                          
  <dd>does a thing for command 2</dd>    
  <dt>command 3</dt>                                                          
  <dd>does a thing for command 3</dd>    
  <dt>command 4</dt>                                                          
  <dd>does a thing for command 4</dd>                                                                            
  <dt>help</dt>                                                                                
  <dd>Shows all available commands</dd>                                                        
</dl>

Upvotes: 5

Related Questions