Tim Molendijk
Tim Molendijk

Reputation: 1046

RxJS equivalent of `Bacon.when()` with properties (which are sampled but not part of the synchronization pattern)

Consider the following Bacon.js code sample (loosely based on the code here, uses bacon.model & bacon.jquery):

<input id="total" type="text" placeholder="total">
/
<input id="quantity" type="text" placeholder="quantity" value="1">
=
<span id="price"></span>
<script>

  var $total = Bacon.$.textFieldValue($("#total")).changes();

  var quantity = Bacon.$.textFieldValue($("#quantity"));

  var price = Bacon.when(
    [$total, quantity], function(x,y) { return x/y; }
  ).toProperty(0);

  price.onValue(function (p) {
    $('#price').text(p);
  });

</script>

What happens here is that the stream $total and the property quantity are joined into a property price. Both $total and quantity get their input from a text input field, but only $total prompts price to be updated. (I.e. quantity is sampled but not part of the synchronization pattern.)

I am trying to achieve this same behavior using RxJS instead of Bacon.js, but all I can come up with is super ugly solutions. I reckon I must be overlooking something…?

My best shot so far (behavior not identical, but you get the idea):

var totalChange = Rx.Observable.fromEvent($('#total'), 'input');

var totalValue = totalChange.map(function () {
  return parseInt($('#total').val(), 10);
});
var quantityValue = totalChange.map(function () {
  return parseInt($('#quantity').val(), 10);
});

var price = Rx.Observable.zip(
  totalValue,
  quantityValue,
  function (x,y) {
    return x/y;
  }
);

price.subscribe(function (p) {
  $('#price').text(p);
});

Because neither combineLatest or zip offer the desired behavior on observables that represent the two text fields directly, all I can think of is to create an observable for quantity based on input events in the text field for total. Which works, but it feels pretty far-fetched.

Any suggestions for alternative approaches?

Upvotes: 3

Views: 782

Answers (3)

Andr&#233; Staltz
Andr&#233; Staltz

Reputation: 13984

Use the recently added withLatestFrom instead of combineLatest.

var price = totalValue.withLatestFrom(quantityValue, (x,y) => x/y);

Compare their diagrams:

Upvotes: 6

Brandon
Brandon

Reputation: 39182

Edit: I just realized this solution is at the bottom of Ben Lesh's answer. I'll leave this here just for the wordier explanation.

If a variable isn't reactive, sometimes the simplest solution is to just acknowledge that and not try to react to it.

Since quantity is not observable, any attempts to make it observable and then combining 2 observable streams is just going to feel hackish.

Instead I'd just embrace its unobservable-ness and use map to read it when total changes:

var totalChange = Rx.Observable.fromEvent($('#total'), 'input');

var totalValue = totalChange.map(function () {
  return parseInt($('#total').val(), 10);
});

var price = totalValue.map(function (total) {
    var quantity = parseInt($('#quantity').val(), 10);
    return total / quantity;
});

price.subscribe(function (p) {
  $('#price').text(p);
});

// or, more succinctly with a single map operation:
var totalChange = Rx.Observable.fromEvent($('#total'), 'input');
var price = totalChange.map(function () {
    var total = parseInt($('#total').val(), 10);
    var quantity = parseInt($('#quantity').val(), 10);
    return total / quantity;
});
price.subscribe(function (p) {
  $('#price').text(p);
});

Upvotes: 0

Ben Lesh
Ben Lesh

Reputation: 108471

EDIT: combineLatest seems to be the equivalent of bacon's when.

I think maybe you're looking to do something like this?

var totalChange = Rx.Observable.fromEvent($('#total'), 'input');

var totalValue = totalChange.map(function () {
  return parseInt($('#total').val(), 10);
});

var quantityValue = totalChange.map(function () {
  return parseInt($('#quantity').val(), 10);
});

Observable.combineLatest(totalValue, quantityValue, function(total, quantity) {
  return total / quantity;
});

Unfortunately, that's the prettiest thing I can think of to answer the spirit of your question.

In reality though, given the example, you'd just do this:

var price = totalChange.map(function() {
  return parseInt($('#total').val(), 10) / parseInt($('#quantity').val(), 10);
});

Upvotes: 1

Related Questions