Let’s extend one of the JavaScript examples from chapter 6 so you can drag around the cat and dog polygons. Three kinds of input mouse events are involved in a drag and drop: mouse down, mouse move, and mouse up (see figure 7.9).
Listing 7.11 defines a BehaviorSubject for the dragging state, which is null when you aren’t dragging. This works, but there’s a risk of bugs, because the drag logic has to make sure dragging is non-null before it does its thing. This is what you’ll improve shortly. Don’t forget that the final subscribe() is the I/O part, which should be kept as far away from the logic as possible (in this case, the bottom of the file).
function insidePolygon(pos, poly) { }
function find(doc, pos) {
Listing 7.11 Dragging cat and dog polygons without flatMapLatest
Event 1: mouse down Mouse button is held down to start dragging
Events 2-9: mouse move With mouse button held down, move events stream in as the mouse is moved.
The object is drawn floating above the document to indicate it is being dragged.
Event 10: mouse up Mouse button is released.
Document is updated with a new position for
this element. Figure 7.9 Modeling the input mouse events in drag-and-drop logic
Code omitted
for (var i = 0; i < doc.length; i++)
if (insidePolygon(pos, doc[i])) return doc[i];
return null;
}
function insert(doc, shape) { doc = doc.slice();
for (var i = 0; i < doc.length; i++)
if (doc[i].id == shape.id) doc[i] = shape;
return doc;
}
function shiftBy(shape, dx, dy) {
var neu = { id: shape.id, coords : [] };
for (var i = 0; i < shape.coords.length; i++) { var pt = shape.coords[i];
neu.coords.push( { x : pt.x + dx, y : pt.y + dy } );
}
return neu;
}
function init() {
var canvas = document.getElementById("myCanvas");
var getXY = function(e) { return { x : e.pageX - canvas.offsetLeft, y : e.pageY - canvas.offsetTop }; };
var sMouseDown = Rx.Observable.fromEvent(canvas, 'mousedown') .map(getXY);
var sMouseMove = Rx.Observable.fromEvent(canvas, 'mousemove') .map(getXY);
var sMouseUp = Rx.Observable.fromEvent(canvas, 'mouseup').map(getXY);
var dragging = new Rx.BehaviorSubject(null);
var doc = new Rx.BehaviorSubject([
{ id: "cat", coords: ... }, { id: "dog", coords: ... }
]);
sMouseDown.withLatestFrom(doc, function(pos, doc) { var shape = find(doc, pos);
if (shape === null) return null;
else return { shape : shape, startPos : pos };
}).merge(
sMouseUp.map(function(pos) { return null; }) ).subscribe(dragging);
sMouseMove.withLatestFrom(dragging, doc, function(pos, dragging, doc) { if (dragging === null) return null;
else {
var dx = pos.x - dragging.startPos.x;
var dy = pos.y - dragging.startPos.y;
return insert(doc, shiftBy(dragging.shape, dx, dy));
}
}).filter(function(doc) { return doc !== null; }) .subscribe(doc);
doc.subscribe(function(doc) { var ctx=canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < doc.length; i++) { var coords = doc[i].coords;
ctx.beginPath();
Numbers omitted Sets the
dragging state if mouseDown clicks a shape
Clears the dragging state on mouseUp Careful! Make
sure you’re dragging!
The I/O part is separate from the logic part.
165 Switch use case #3: removing invalid states
ctx.moveTo(coords[0].x, coords[0].y);
for (var j = 0; j < coords.length; j++) ctx.lineTo(coords[j].x, coords[j].y);
ctx.closePath();
ctx.fillStyle = '#D090ff';
ctx.fill();
} });
}
To run this example, check this out if necessary: git clone https://github.com/
SodiumFRP/sodium. Then point your browser at sodium/book/web/drag1.html.
7.8.1 And now, improved with flatMapLatest
In functional programming, you strive to make it so invalid states aren’t represent- able. What you want is for the dragging logic not to exist at all until you’re dragging.
You can replace the code in bold from the previous listing with the code in listing 7.12. When you mouse-down on a valid shape, you output an observable that describes what to do from then on.
takeUntil() is an RxJS operation that terminates the observable when an event arrives on its argument, sMouseUp. It has no direct equivalent in Sodium, but you could write it. This does what you want: it instantiates the drag logic when you start the drag and destroys it when you stop. The drag logic can safely assume it’s always drag- ging. The invalid state of mouse move when not dragging is eliminated, and so is any associated potential bug.
var dragging = ... ; var doc = ... ;
sMouseDown.withLatestFrom(doc, function(pos, doc) { return { startPos : pos, shape : find(doc, pos) };
}).filter(function(x) { return x.shape !== null; }) .flatMapLatest(function(x) {
var startPos = x.startPos;
var shape = x.shape;
return sMouseMove.withLatestFrom(doc, function(pos, doc) { var dx = pos.x - startPos.x;
var dy = pos.y - startPos.y;
return insert(doc, shiftBy(shape, dx, dy));
}).takeUntil(sMouseUp);
}).subscribe(doc);
To run this example, check this out if necessary: git clone https://github.com/
SodiumFRP/sodium. Then point your browser at sodium/book/web/drag2.html.
Listing 7.12 Dragging cat and dog polygons: improved with flatMapLatest