Timing in creation of EventStreams - how to pass around a (promise of a) stream before its source could be created?

30 views
Skip to first unread message

Dmitry Kourmyshov

unread,
Mar 23, 2016, 12:25:07 PM3/23/16
to Bacon.js
Hello! I am now using Bacon.js for first time, and run into following problem:

I defnied several components, subscribing to one another's request through EventStreams. One of components wraps OpenLayers map, another is an information panel for that map. The panel should display information about map's current state; the difficulty is that the map is rendered after information panel construction.

Simplified code:

function MapComponent() {
 
var map;
 
this.render = function () {
    map
= new ol.Map(...);
   
this.coords = Bacon.fromEvent(map, "moveend").map(...).toProperty();
 
}
}

function InfoPanelComponent(mapComponent) {
  mapComponent
.coords.onValue(... /* display value */ );
 
/* as components are constructed before map render, the coords stream is undefined at the moment of creation */
}

var mapComponent = new MapComponent();
var infoPanelComponent = new InfoPanelComponent(mapComponent);
mapComponent
.render();


I see two different solutions to this: my first thought was to create metastream, through which InfoPanelComponent will receive coords stream when it will be ready. This feels nice but maybe unnesecarry complicated. Another was proposed by colleague of mine: to create coords stream in MapComponent constructor and push events into it from render(), e.g.

function MapComponent() {
 
var map;
 
var coordsEvent = _.clone(Backbone.Events);
 
this.coords = Bacon.fromEvent(coordsEvent, 'change').map(...).toProperty();;

 
this.render = function () {
    map
= new ol.Map(...);
    map
.on("moveend", function (event) { coordsEvent.trigger("change", event); }
 
}
}

This one seems more awkward to me.
What is most baconite solution here?

Sincerely,
Dmitry

Juha Paananen

unread,
Mar 24, 2016, 2:57:51 AM3/24/16
to Dmitry Kourmyshov, Bacon.js
This seems like a “chicken-egg” problem: you cannot define all of your streams (here, "coords") before you need to reference them (in InfoPanelComponent). The most “baconite” solution would be to get back to the situation where you can define the streams first and use them later. But I guess you cannot move the “new ol.Map()” call to the MapComponent constructor, can you? 

If not, you need a workaround like yours or your colleague’s. The most “baconite” workaround would be a Bacon.Bus. You can for instance

function MapComponent() {
  var coordsBus = new Bacon.Bus()
  this.coords = coordsBus.toProperty()

  this.render = function () {
    var map = new ol.Map(...);
    coordsBus.plug(Bacon.fromEvent(map, "moveend").map(…))
  }
}

It’s essentially the same solutions as your colleague’s except using a Bacon.Bus instead of Backbone.Events.

Here’s my old post about the chicken-egg-problem btw: http://baconjs.blogspot.fi/2013/02/chicken-egg-and-baconjs.html

Best Regards,
Juha

--
You received this message because you are subscribed to the Google Groups "Bacon.js" group.
To post to this group, send email to bac...@googlegroups.com.
Visit this group at https://groups.google.com/group/baconjs.

Dmitry Kourmyshov

unread,
Mar 24, 2016, 3:52:54 AM3/24/16
to Bacon.js, dmitry.k...@gmail.com
Thank you! I tried to avoid Buses (having read http://baconjs.blogspot.ru/2014/12/bus-of-doom.html), but proxy events seem no better, and Buses at least make for cleaner code, so for now I'll probably stick with them.

четверг, 24 марта 2016 г., 9:57:51 UTC+3 пользователь raimohanska написал:

Dmitry Kourmyshov

unread,
Apr 4, 2016, 7:20:54 PM4/4/16
to Bacon.js, dmitry.k...@gmail.com
I've found another solution, which seems to me most "pure" (especially if Bus is considered harmful): instead of calling render as a method, trigger it through a stream (I now provide more full snippet from my app, hopefully details wouldnt distract from Bacon parts):

function MapComponent(coreStreams) {
 
var render = function () {
   
var view = new ol.View({center: ol.proj.fromLonLat([37.41, 8.82]), zoom: 4});
   
var map = new ol.Map({target: 'map', view: view, layers:[new ol.layer.Tile({source: new ol.source.MapQuest({layer: 'sat'})})]});
   
return {coords: Bacon.fromEvent(view, 'change:center').map(function (e) {return ol.proj.toLonLat(e.target.get(e.key));})};
 
};
 
var renderResults = Bacon.when([coreStreams.render], render);
  renderResults
.onValue(_.noop); //without this, map wouldn't render
 
this.output = {coords: renderResults.flatMap(_.property("coords")).toProperty([0, 0])};
}

function InfoPanelComponent(coreStreams, mapStreams) {
 
var render = function (render, coords) {
   
return new Ractive({
      el
: 'infoPanel',
     
template: 'Coords: {{coords[0]}}, {{coords[1]}}',
      data
: {coords: coords}
   
});
 
};
 
var renderResults = Bacon.when([coreStreams.render, mapStreams.coords], render);
  renderResults
.onValue(_.noop); //ditto for infoPanel - without this "sink", it wouldnt render until first event come to the function below
 
 
Bacon.when([renderResults.toProperty(), mapStreams.coords.changes()], function (ractive, coords) {
    ractive
.set('coords', coords);
 
}).onValue(_.noop); // agan - without the subscriber, changes wouldn't be processed
}

var coreStreams = {render: Bacon.later(100, true)};
var mapStreams = new MapComponent(coreStreams).output;
new InfoPanelComponent(coreStreams, mapStreams);


However, I have two problems here:

First, I had to add artificial onValue() "sinks", because when() on itself doesn't considered subscription.

Second, flatMap() of Properties is EventStream, not Property, so I had to add .toProperty outside of render() - hence, I cannot set initial value of property based on results of first render. In my example, I just could use same hard-coded coordinates I used in construction of View, but in other components initial values would be computed during render somehow.

I like this variant better, but feel it can be improved further.

Sincerely,
Dmitry



четверг, 24 марта 2016 г., 9:57:51 UTC+3 пользователь raimohanska написал:
This seems like a “chicken-egg” problem: you cannot define all of your streams (here, "coords") before you need to reference them (in InfoPanelComponent). The most “baconite” solution would be to get back to the situation where you can define the streams first and use them later. But I guess you cannot move the “new ol.Map()” call to the MapComponent constructor, can you? 
Reply all
Reply to author
Forward
0 new messages