I'm trying to understand bacon.js and FRP so tried to make a simple drag and drop example, but I'm having trouble with the lazy evaluation of one piece of code. When I add a .log()
into the stream it seems to look and act fine, but if I take it out, it doesn't update. Here is what I'm doing:
// UI streams
block_mousedown = block_el.asEventStream('mousedown').map(xyFromEvent);
global_mousemove = html.asEventStream('mousemove').map(xyFromEvent);
global_mouseup = html.asEventStream('mouseup');
// Composites
isDragging = block_mousedown.merge(global_mouseup.map(0));
mouseDragging = Bacon.combineAsArray(isDragging, global_mousemove)
.filter(function(v){ return notZero(v[0]) })
mouseDeltaFromClick = mouseDragging
.map(getDelta)
// Block offset when it was clicked on
block_pos_at_mousedown = block_mousedown
.map( function(a,b){ return block_el.offset();})
.map(function(e){ return [e.left, e.top]; })
// If I remove this log(), it doesn't evaluate
.log();
// merge mouse delta with block position when clicked
mouseDeltaAndBlockPos = mouseDeltaFromClick
.combine(block_pos_at_mousedown, '.concat')
.onValue( function(e){
block_el.css({
top : e[3]+e[1]+"px",
left : e[2]+e[0]+"px"
});
});
And here is a jsFiddle of it
I'm thinking I might be going about this all wrong, is this even the right approach? I want to pass through the position of the block when it was clicked which should update on mousedown
but not be updated along with the mousemove
.
The behavior you are describing has little to do with lazy evaluation: root of the problem is order of execution.
In your code (without the log()
on block_pos_at_mousedown
) mouseDeltaFromClick
seems to change before block_pos_at_mousedown
(I must say that I don't know how exactly log()
changes the order). Let's hold on that.
The observable.combine
method expects Property
as a first argument - EventStream
you have passed will be automatically converted. Now, mouseDeltaAndBlockPos
changes (and consequently fires all callbacks) whenever mouseDeltaFromClick
or block_pos_at_mousedown
changes.
So, when mouseDeltaFromClick
fires before block_pos_at_mousedown
the callback at the end of code is called with new delta but with old block position (because back_pos_at_mousedown
was converted to Property
). The old value is [0,0]
so the block snaps to the top left corner on each click.
How to fix it? The safe way is to assume nothing about order of execution of unrelated callbacks and write it again with this in mind. I came up with this:
function xyFromEvent(v){ return [v.clientX, v.clientY]; }
function getDelta(t){
var a = t[1];
var b = t[0];
return [a[0]-b[0], a[1]-b[1]];
}
function add(p1, p2) {
return [p1[0] + p2[0], p1[1] + p2[1]];
}
$().ready(function () {
var block = $("#clickable-block");
var html = $("html");
var blockDragging = block.asEventStream('mousedown').map(true)
.merge(html.asEventStream('mouseup').map(false))
.toProperty(false);
var deltas = html.asEventStream('mousemove').map(xyFromEvent).slidingWindow(2,2).map(getDelta);
// Just like deltas, but [0,0] when user is not dragging the box.
var draggingDeltas = Bacon.combineWith(function(delta, dragging) {
if(!dragging) {
return [0, 0];
}
return delta;
}, deltas, blockDragging);
var blockPosition = draggingDeltas.scan([0,0], add);
blockPosition.onValue(function(pos) {
block.css({
top : pos[1] + "px",
left : pos[0] + "px"
});
});
});
And jsFiddle: http://jsfiddle.net/aknNh/25/
EDIT: In comments raimohanska had suggested another solution using flatMap
: http://jsfiddle.net/TFPge/1/