combineWith vs onValues in Bacon.js

497 Views Asked by At

I have 2 Bacon.jQuery Models (but the problem can be illustrated with any Bacon Observable).

I have 3 combo boxes: foo, bar and quux. bar depends on foo and quux depends on a combination of bar and quux. There is a findQuux function which finds a value for quux combobox from foo and bar.

If a user changes foo combobox, the values in bar and quux boxes are selected. Here is a working implementation of quux selection:

// WORKS
var quuxBus = new Bacon.Bus()
Bacon.onValues(fooModel, barModel, function (foo, bar)
{
    quuxBus.push(findQuux(foo, bar))
})
quuxModel.addSource(quuxBus.toProperty('quux4'))

There is a little problem with this solution as I need 'quux4' hack to set quux correctly on page load. But the following simpler solution doesn't work at all:

// DOESN'T WORK
var quuxCombo = Bacon.combineWith(findQuux, fooModel, barModel)
quuxModel.addSource(quuxCombo)

The problem is that findQuux expects a valid combination of foo and bar and crashes when fed with an impossible combination for which a quux cannot be found.

The onValues/push solution works because findQuux is called only once when a user changes foo combobox. The bacon.combineWith solution doesn't work because findQuux is called twice.

What is the recommended way to implement the data source for quuxModel? Coding findQuux defensively is not an option.

The full code can be found at http://jsfiddle.net/5zp4D/8/

Update: There are already sensible values for foo and bar from page load. As can be seen from the fiddle link fooModel is initialized with an explicit value, and barModel is recalculated from that initial value:

var fooModel = Bacon.$.selectValue(fooDom, 'foo2-value')
var barModel = Bacon.$.selectValue(barDom)

barModel.addSource(fooModel.map(function (x)
{
    return json[x][1].val
}))

foo and bar never take invalid values. Moreover, bar is repopulated once a user changes foo, so only valid combinations are seen in the UI.

The problem with combineWith approach is that when a user switches foo then findQuux function is called twice with intermediate values, and one of the intermediate combinations is incorrect, while the combination constituents are separately correct. I changed the fiddle to better illustrate the problem: http://jsfiddle.net/5zp4D/12/

As can be seen from var json the valid combinations are 1-1, 1-2, 2-3, 2-4 and 2-5.

I uncommented the broken version, added logging. When you select foo1 and then select back foo2 in the first combobox, you get 4 messages in the log instead of 3:

On page load you see that quux value is correctly initialized (2-4):

"valid combination of foo2-value and bar4-value"

When you select foo1 instead of foo2 you see that findQuuxDef is called with 1-4 (foo is new, bar is old) and then with 2-4 (foo is new, bar is new):

"invalid combination of foo1-value and bar4-value"
"valid combination of foo1-value and bar2-value"

My problem is that invalid intermediate combinations don't happen with onValues/push approach, and I'd like to know what is an idiomatic recommended approach to UI with linked elements.

1

There are 1 best solutions below

4
On

I'd start with having quuxCombo only output valid values for findQuux. I'd prefer having sensible values for inputs foo and bar from page load, but if that's not possible (why not?), I'd use filter on the inputs. For instance,

var quuxCombo = Bacon.combineWith(findQuux, fooModel.filter(validFoo), barModel.filter(validBar))

I guess though, that you want to start with some initial value before valid input exists. For this, I added a new method Property.startWith into Bacon.js 0.6.15. So, now you can

var quuxCombo = Bacon.combineWith(findQuux, fooModel.filter(validFoo), barModel.filter(validBar)).startWith("quux4")