Reputation: 20580
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
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:
flatMap
to listen to the definitions
stream, each value d
will be an matched element to manipulate.flatMap
again to capture each event (mouseOver and mouseOut) and project instead the observable for our matched elements.tap
.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.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