Frage

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.

War es hilfreich?

Lösung

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/

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top