Implementing Drag and Drop Using Backbone and EaselJS
In this article, we are going to build a simple drag and drop application using EaselJS and Backbone.js. Backbone will give structure to our application by providing models, collections, and views. Easel will make working with the HTML5 canvas
element easy. Although we don’t necessarily need Backbone for such a simple application, it is fun to get started with Backbone in this way.
Get Started
First, we create our directory structure as follows:
.
|-- index.html
+-- js
|-- main.js
|-- models
| +-- stone.js
+-- views
+-- view.js
Next, in index.html
include the JavaScript files and a canvas
element, as shown in the following code sample. Once this is done, we’re ready to manipulate the canvas
.
<body>
<!-- Canvas Element -->
<canvas id="testcanvas" height="640" width="480"/>
<script src="/bower_components/jquery/jquery.min.js"></script>
<!-- underscore is needed by backbone.js -->
<script src="/bower_components/underscore/underscore-min.js"></script>
<script src="/bower_components/backbone/backbone.js"></script>
<script src="/bower_components/easeljs/lib/easeljs-0.7.1.min.js"></script>
<!-- tweenjs is for some animations -->
<script src="/bower_components/createjs-tweenjs/lib/tweenjs-0.5.1.min.js"></script>
<script src="/js/models/stone.js"></script>
<script src="/js/views/view.js"></script>
<script src="/js/main.js"></script>
</body>
Backbone Models
By creating a Backbone model, we will have key-value bindings and custom events on that model. This means we can listen to changes for model properties and render our view accordingly. A Backbone collection, is an ordered set of models. You can bind change
events to be notified when any model in the collection changes. Next, let’s create a stone model, and a stone collection. The following code belongs in js/models/stone.js
.
var Stone = Backbone.Model.extend({
});
var StoneCollection = Backbone.Collection.extend({
model: Stone
});
Initialize the Backbone View Using EaselJS
Backbone views don’t determine anything about HTML, and can be used with any JavaScript templating library. In our case we aren’t using a templating library. Instead, we manipulate the canvas
. You can bind your view’s render()
function to the model’s change
event so that when the model data changes, the view is automatically updated.
To get started with Easel, we create a stage that wraps the canvas
element, and add objects as children. Later, we pass this stage to our backbone view. The code in js/main.js
that accomplishes this is shown below.
$(document).ready(function() {
var stage = new createjs.Stage("testcanvas");
var view = new CanvasView({stage: stage}).render();
});
We’ve created our CanvasView
and called its render()
function to render it. We will revisit the implementation of render()
shortly. First, let’s see our initialize()
function, which is defined in js/views/view.js
.
var CanvasView = Backbone.View.extend({
initialize: function(args) {
// easeljs stage passed as argument.
this.stage = args.stage;
// enableMouseOver is necessary to enable mouseover event http://www.createjs.com/Docs/EaselJS/classes/DisplayObject.html#event_mouseover
this.stage.enableMouseOver(20);
// stone collection
this.collection = new StoneCollection();
// bounds of pink area and our stones. the pink area is called "rake".
this.rakeOffsets = {
x: 10,
y: 400,
height: 150,
width: 300,
stoneWidth: 50,
stoneHeight: 50
};
// listen to collection's add remove and reset events and call the according function to reflect changes.
this.listenTo(this.collection, "add", this.renderStone, this);
this.listenTo(this.collection, "remove", this.renderRake, this);
this.listenTo(this.collection, "reset", this.renderRake, this);
},
//...
});
listenTo()
listens for model/collection changes and calls the function passed as the second argument. We pass the context the function is being called in as a third argument. When we add a stone to our collection, an add
event will dispatch this.renderStone()
and pass the new stone to the function. Similarly, when the collection is reset, a reset
event will dispatch this.renderRake()
. By implementing these render functions, the view will always be in sync with the collection.
Rendering the View
The render()
function, shown below, just calls this.renderRake()
and updates the stage.
render: function() {
this.renderRake();
// stage.update is needed to render the display to the canvas.
// if we don't call this nothing will be seen.
this.stage.update();
// The Ticker provides a centralized tick at a set interval.
// we set the fps for a smoother animation.
createjs.Ticker.addEventListener("tick", this.stage);
createjs.Ticker.setInterval(25);
createjs.Ticker.setFPS(60);
},
The renderRake()
method, which is also stored in js/views/view.js
, is shown below.
renderRake: function() {
// http://stackoverflow.com/questions/4886632/what-does-var-that-this-mean-in-javascript
var that = this;
// create the rake shape
var rakeShape = new createjs.Shape();
rakeShape.graphics.beginStroke("#000").beginFill("#daa").drawRect(this.rakeOffsets.x, this.rakeOffsets.y, this.rakeOffsets.width, this.rakeOffsets.height);
// assign a click handler
rakeShape.on("click", function(evt) {
// When rake is clicked a new stone is added to the collection.
// Note that we add a stone to our collection, and expect view to reflect that.
that.collection.add(new Stone());
});
// add the shape to the stage
this.stage.addChild(rakeShape);
// a createjs container to hold all the stones.
// we hold all the stones in a compound display so we can
// easily change their z-index inside the container,
// without messing with other display objects.
this.stoneContainer = new createjs.Container();
this.stage.addChild(this.stoneContainer);
// for each stone in our collection, render it.
this.collection.each(function(item) {
this.renderStone(item);
}, this);
},
renderRake()
does two things. First, it renders the rake shape (pink rectangle) on the canvas, and creates a click
handler on it. Second, it traverses the stone collection and calls renderStone()
on each item. The click
handler adds a new stone to the collection.
Next, let’s look at the renderStone()
function.
renderStone: function(model) {
// var that = this;
var baseView = this;
// build the stone shape
var stoneShape = buildStoneShape();
// make it draggable
// the second argument is a callback called on drop
// we snap the target stone to the rake.
buildDraggable(stoneShape, function(target, x, y) {
rakeSnap(target, false);
});
// add the stone to the stage and update
this.stoneContainer.addChild(stoneShape);
this.stage.update();
function buildStoneShape() {
var shape = new createjs.Shape();
shape.graphics.beginStroke("#000").beginFill("#ddd").drawRect(0, 0, baseView.rakeOffsets.stoneWidth, baseView.rakeOffsets.stoneHeight);
return shape;
};
},
We’ve called the buildDraggable()
function to make the stone draggable. We will see how to implement that next. But first, let’s review how our backbone view works. The CanvasView
listens to the collection’s add
event, and when a new stone is added, it calls renderStone()
. The render()
method renders the rake and calls renderStone()
on each stone in the collection. When the rake is clicked, a new stone model is added to the stone collection, and then renderStone()
is called on the new stone.
Now, let’s look at the buildDraggable()
function that implements the drag and drop functionality:
renderStone: function(model) {
// ...
function buildDraggable(s, end) {
// on mouse over, change the cursor to pointer
s.on("mouseover", function(evt) {
evt.target.cursor = "pointer";
});
// on mouse down
s.on("mousedown", function(evt) {
// move the stone to the top
baseView.stoneContainer.setChildIndex(evt.target, baseView.stoneContainer.getNumChildren() - 1);
// save the clicked position
evt.target.ox = evt.target.x - evt.stageX;
evt.target.oy = evt.target.y - evt.stageY;
// update the stage
baseView.stage.update();
});
// on mouse pressed moving (drag)
s.on("pressmove", function(evt) {
// set the x and y properties of the stone and update
evt.target.x = evt.target.ox + evt.stageX;
evt.target.y = evt.target.oy + evt.stageY;
baseView.stage.update();
});
// on mouse released call the end callback if there is one.
s.on("pressup", function(evt) {
if (end) {
end(evt.target, evt.stageX + evt.target.ox, evt.stageY + evt.target.oy);
}
});
};
// ...
},
And for the constraint of snapping the stone to the rake, here are the final utility functions we need.
// drag the stone, either by animating or not
function dragStone(s, x, y, animate) {
if (animate) {
// Use tween js for animation.
createjs.Tween.get(s).to({x: x, y: y}, 100, createjs.Ease.linear);
} else {
// set x and y attributes without animation
s.x = x;
s.y = y;
}
// update
baseView.stage.update();
};
// calculate x position to snap the rake
function snapX(x) {
if (x < baseView.rakeOffsets.x) {
x = baseView.rakeOffsets.x;
} else if (x > baseView.rakeOffsets.x + baseView.rakeOffsets.width - baseView.rakeOffsets.stoneWidth) {
x = baseView.rakeOffsets.x + baseView.rakeOffsets.width - baseView.rakeOffsets.stoneWidth;
}
return x;
};
// calculate y position to snap the rake
function snapY(y) {
if (y < baseView.rakeOffsets.y) {
y = baseView.rakeOffsets.y;
} else if (y > baseView.rakeOffsets.y + baseView.rakeOffsets.height - baseView.rakeOffsets.stoneHeight) {
y = baseView.rakeOffsets.y + baseView.rakeOffsets.height - baseView.rakeOffsets.stoneHeight;
}
return y;
};
// drag stone within the rake bounds. animation is disabled if second argument is given. animation is enabled by default
function rakeSnap(s, animateDisabled) {
dragStone(s, snapX(s.x), snapY(s.y), !animateDisabled);
};
Conclusion
In conclusion, Backbone is not restricted to DOM manipulation, and can be used anywhere that needs model-view structure. Though it can be used to build single page applications, it is not a complete framework, and we have only seen one side of Backbone in this article. If you like to use Backbone for large scale applications, I suggest using Marionette.js, which handles some primitive problems with Backbone.
The full code for this article can be found on GitHub. A live demo is also available on Heroku. To get started, just click on the pink area to create a draggable stone. The stone will be draggable, and it will be constrained inside the pink area.