Search code examples
javascriptsystem.reactivereactive-programmingrxjsreactive-extensions-js

I have a chain of Rx.Subjects (A->B->C->A), but the final step is not working


Live example.

I'm completely new to Rx*. I'm trying to create a reactive version of MVC using RxJS for my thesis. It's loosely based on https://github.com/staltz/mvi-example

I probably should've studied RxJS more before starting to code, but I've realised I usually learn the best by just jumping to the deep end of the pool. Now I'm completely stuck however.

The View has a text field with a numeric value and a button. Pressing the button changes the color of the text, clicking the text iterates the numeric value. The value is stored in the model, so that "chain" goes through all three objects (View->Controller->Model->View). Pressing the button does not require the model, so that chain is View->Controller->View.

Everything else works, but the final part of the chains (->View) is giving me trouble. Clicking on the text (in the View) propagates changes to the Controller and to the Model, but the View is not notified of the change in the Model. I don't understand why, since the way I understand this all these are implemented in the same way.

I've added comments to the code sample below to designate the functions that don't get called.

The code is available here. Note that, like I said, the code is for my thesis so some design decisions and the app itself may seem odd. Below I've also tried to include the parts I feel are most relevant. I know I should provide a complete example, but it's kind of impossible in this case.

The "obs" parameter that objects get is an internal data structure that I use to store observables. The relevant part is

var OBSERVABLES = {
    observe: function(source, observer, observer_name) {
      this.request(source, function(s) {
        console.log("\t[" + source + " is being observed by " + observer_name + " (" + s.constructor.name + " -> " + observer.constructor.name + ")]")

        return s.subscribe(
          function myOnNext(x) {
            console.log("\t(" + source + ") " + s.constructor.name + " -> OnNext -> (" + observer_name + ") " + observer.constructor.name)
            observer.onNext(x)
          },
          function myOnError(err) {
            console.log(error)
          })
      })
    },

View.js

var modelAmount = new Rx.Subject()
var textClicks = new Rx.Subject()
var buttonClicks = new Rx.Subject()
var changeColors = new Rx.Subject()

function View(obs, div) {
  console.log("View :: New View")
  obs = obs
  div = div
  var self = this

  obs.observe("clickAmount", modelAmount, "modelAmount")
  obs.observe("changeColor", changeColors, "changeColors")
  obs.add("textClicks", textClicks)
  obs.add("buttonClicks", buttonClicks)

  //Draw HTML elements
  render(div);
}
...
function onClick() {
  console.log("View :: onClick() emits 'textClicks'");
  textClicks.onNext()
}

function onButtonClick() {
  console.log("View :: onButtonClick() emits 'buttonClicks'");
  buttonClicks.onNext()
}

//Listen to controllers instruction to change the color
// DOES NOT WORK
var changeNumberColor = changeColors.map(function(c) {
  console.log("View :: Listened to 'changeColor'")
  if (c === undefined) {
    c = getColor()
  }

  CONTENT.css('color', c)
});

//Listen to model's instruction to change the value
// DOES NOT WORK
var setValue = modelAmount.last(function(latestValue) {
  console.log("View :: Listened to 'clickAmount'")
  console.log("View :: setValue(" + latestValue + ")")
  CONTENT.text(latestValue)
});

Controller.js

  var inputTextClicks = new Rx.Subject();
  var inputButtonClicks = new Rx.Subject();

  var obs = null;

  function Controller(obs){
    console.log("Controller :: New Controller")
    obs = obs

    obs.observe("textClicks", inputTextClicks, "inputTextClicks")
    obs.observe("buttonClicks", inputButtonClicks, "inputButtonClicks")

    obs.add("addAmount", addAmount);
    obs.add("changeColor", changeColor);
  }

  //Listen to input events and give instructions to model
  var addAmount = inputTextClicks.map(function(){
    console.log("Controller :: addAmount() listened to 'textClicks' and emits 'addAmount'")
    return 1;
  });

  //Listen to button presses and give instructions to the view
  var changeColor = inputButtonClicks.map(function(){
    console.log("Controller :: ChangeColor() listened to 'buttonClicks' and emits 'changeColor'")
    return 1;
  });

Model.js

  var controllerAddAmount = new Rx.Subject();

  var obs = null;

  //Stores the texts value. Starts at 0
  var VALUE = 0;

  function Model(obs){
    console.log("Model :: New Model")
    obs = obs

    obs.observe("addAmount", controllerAddAmount, "controllerAddAmount")
    obs.add("clickAmount", amountChanged)
  }

  //Listen to the controller about changing the value, notify the view about the new value
  var amountChanged = controllerAddAmount.map(function(val){
    console.log("Model :: amountChanged() listened to 'addAmount' and emits 'clickAmount'")
    VALUE += val
    console.log("Model :: Amount Changed to " + VALUE)
    return VALUE;
  })

So the problem is View.setValue() and View.setNumberColor().


Solution

  • You have a lot of code that essentially looks like this:

    // Listen to x to ...
    var a = x.map(function (val) { ... });
    

    But then you never show what you do with a. map takes an observable and produces a new observable that will run the supplied mapping operation. However, the observable it returns is inert and does not actually do anything unless you eventually subscribe to a (or some derivative of a).

    It looks like for your controller and model you eventually do subscribe to the observables they are producing (through the circuitous route of adding them to your obs and then subscribing to them in your view).

    However, it isn't clear you ever subscribe to changeNumberColor or modelAmount. Based upon the imperative code within those two map methods, I'm guessing you should be using subscribe instead of map in those 2 cases.

    Also, for modelAmount you are using the last operator incorrectly. Most likely you think last does something different than it does.