Scoping issue in backbone view - backbone.js

I am in the process of getting a simple drawing view created using Easeljs inside of a Backbonejs view. I am having an issue with scope, in my listener events, for example:
this.stage.addEventListener("stagemousedown", this.handleMouseDown);
I started to started doing this to get around my scope issue
var self = this;
this.stage.addEventListener("stagemousedown", function(){
var foo = self.bar;
});
However, this seems sloppy, especially since the sample code I am porting (http://www.createjs.com/#!/EaselJS/demos/drawing), has nested levels of eventListeners.
SketchView = Backbone.View.extend({
initialize: function(){
this.canvas;
this.stage;
this.drawingCanvas;
this.oldPt;
this.oldMidPt;
this.title;
this.color;
this.stroke;
this.colors;
this.index;
},
beforeRender : function (){
this.template = _.template( tpl.get(this.templateFile) );
},
render: function(eventName) {
$(this.el).html(this.template(this.model));
return this;
},
//add in UI
afterRender : function (){
this.createUI();
},
createUI: function() {
this.canvas = document.getElementById("demoCanvas");
this.index = 0;
this.colors = ["#828b20", "#b0ac31", "#cbc53d", "#fad779", "#f9e4ad", "#faf2db", "#563512", "#9b4a0b", "#d36600", "#fe8a00", "#f9a71f"];
//Create a stage by getting a reference to the canvas
this.stage = new createjs.Stage(this.canvas);
this.stage.autoClear = false;
this.stage.enableDOMEvents(true);
createjs.Touch.enable(this.stage);
createjs.Ticker.setFPS(24);
this.stage.addEventListener("stagemousedown", this.handleMouseDown);
this.stage.addEventListener("stagemouseup", this.handleMouseUp);
this.title = new createjs.Text("Click and Drag to draw", "36px Arial", "#777777");
this.title.x = 300;
this.title.y = 200;
this.stage.addChild(this.title);
this.stage.addChild(this.drawingCanvas);
this.stage.update();
},
handleMouseDown: function (event) {
if (this.stage.contains(this.title)) { this.stage.clear(); this.stage.removeChild(this.title); }
this.color = this.colors[(this.index++)%this.colors.length];
this.stroke = Math.random()*30 + 10 | 0;
this.oldPt = new createjs.Point(this.stage.mouseX, this.stage.mouseY);
this.oldMidPt = this.oldPt;
this.stage.addEventListener("stagemousemove" , this.handleMouseMove);
},
handleMouseMove: function (event) {
var midPt = new createjs.Point(this.oldPt.x + this.stage.mouseX>>1, this.oldPt.y+this.stage.mouseY>>1);
this.drawingCanvas.graphics.clear().setStrokeStyle(this.stroke, 'round', 'round').beginStroke(this.color).moveTo(midPt.x, midPt.y).curveTo(this.oldPt.x, this.oldPt.y, this.oldMidPt.x, this.oldMidPt.y);
this.oldPt.x = this.stage.mouseX;
this.oldPt.y = this.stage.mouseY;
this.oldMidPt.x = midPt.x;
this.oldMidPt.y = midPt.y;
this.stage.update();
},
handleMouseUp: function (event) {
this.stage.removeEventListener("stagemousemove" , this.handleMouseMove);
}
});
An unrelated question, is should I be instantiating my variables in the initialize function? I am still new to Backbone, and trying to figure out best practices.

Backbone requires the Underscore library, and I strongly recommend becoming familiar with it : it has a lot of great stuff. In particular it has a _.bind method that you can use like so:
this.stage.addEventListener("stagemousedown", _(function(){
var foo = this.bar; // "this" will be correct now thanks to bind
}).bind(this);
It also has a related _.bindAll method, which can be used (often inside of an initialize function) to bind an object's methods to it, like so:
initialize: function() {
_(this).bindAll('handleMouseDown'); // handleMouseDown's this will be correct
this.stage.addEventListener("stagemousedown", this.handleMouseDown);
}
However, you can avoid all of this and let Backbone do the binding for you if you just take advantage of its event handling:
Backbone.View.extend({
events: {mousedown: 'handleMouseDown'}
handleMouseDown: function() {
// this will be bound correctly
As for your question about instantiating variables inside initialize, the answer is ... probably, but it's hard to say without knowing your specific context. In general, initialize should do anything that needs to happen when the Collection/Model/View gets created, so if initializing variables falls under that then sure.

Related

BackboneCollection.Unshift(NewItem) - not working

i have a Backbone collection and it was hold by a knockback.ObservableCollection and i want to add he new items on the Top just like Array.Shift(NewItem). Is there anyway to do that? i pretty new with backbonejs
this.recentItemList = kb.collectionObservable(recentItems, {
view_model: RecentItemViewModel, sort_attribute: 'modifiedDateTime'
});
var options = {};
var self = this;
var newRecentItem = new recentItem
({
recentId: model.get("id"),
recentName: model.get("description"),
type: "test"
});
options.success = function ()
{
self.recentItems.unshift(newRecentItem);
};
newRecentItem.save({}, options);
Make your viewModel variable a ko.observableArray, and you can then just variableName.unshift(newElement);
Okay, my mistakes.
so i just removed the sort_attribute and it works like a charm.

ng-if only works when referencing var direct from service, instead of var in controller scope

I am trying to understand why my ng-if statement doesn't work when I reference a local variable in my controller that is assigned to a value from a service, but it works properly if assigned directly to the value from that service.
For example, this works:
<div class="map" ng-if="interactiveMap.mapService.esriLoaded">
<esri-map id="map1"
map-options="interactiveMap.mapOptions"
load="interactiveMap.load"
register-as="interactiveMap">
</esri-map>
</div>
with the following controller:
angular.module('tamcApp')
.controller('InteractivemapCtrl', function (map, config) {
var self = this;
self.map = {};
self.mapService = map;
self.mapOptions = {
basemap: 'mcgiStreet',
extent: config.globals.initialExtent,
sliderStyle: 'small'
};
self.load = function(){
map.getMap('interactiveMap').then(function(thisMap) {
console.log(thisMap);
self.map = thisMap;
});
};
});
But if I were to assign the "esriLoaded" var to a local var in the scope, like this:
<div class="map" ng-if="interactiveMap.esriLoaded">
<esri-map id="map1"
map-options="interactiveMap.mapOptions"
load="interactiveMap.load"
register-as="interactiveMap">
</esri-map>
</div>
Controller here:
angular.module('tamcApp')
.controller('InteractivemapCtrl', function (map, config) {
var self = this;
self.map = {};
self.esriLoaded = map.esriLoaded;
self.mapOptions = {
basemap: 'mcgiStreet',
extent: config.globals.initialExtent,
sliderStyle: 'small'
};
self.load = function(){
map.getMap('interactiveMap').then(function(thisMap) {
console.log(thisMap);
self.map = thisMap;
});
};
});
Then it doesn't work. The value for "esriLoaded" is always false (which is the default value for esriLoaded). It's like it isn't updating the value of self.ersiLoaded when the value gets updated in the "map" service. Here is the code for the "map" service, just in case folks need it to answer this question.
angular.module('tamcApp')
.service('map', function (config, esriLoader, esriRegistry, esriMapUtils) {
// AngularJS will instantiate a singleton by calling "new" on this function
var self = this;
self.esriLoaded = false;
self.lazyload = function() {
// Make a call to load Esri JSAPI resources.
// A promise is provided for when the resources have finished loading.
esriLoader.bootstrap({
url: config.globals.esriJS
}).then(function() {
// Set Loaded to be true
self.esriLoaded = true;
// DEFINE CUSTOM BASEMAP USED BY ALL MAPS
esriMapUtils.addCustomBasemap('mcgiStreet', {
urls: ['http://myhost.com/arcgis/rest/services/BaseMap/StreetMap/MapServer'],
title: 'MCGI Street Map',
thumbnailurl: ''
});
});
};
if (!self.esriLoaded) {
self.lazyload();
}
self.getMap = function(id){
return esriRegistry.get(id);
};
});
That is actually not because of angular, but because of JavaScript. map.esriLoaded is a boolean value, a primitive and thus not an object, which leads to your local self.esriLoaded not becoming a reference (as only objects can be referenced), but just a plain copy of the boolean value contained in map.esriLoaded.
A short example to make it more clear:
//Primitive
var a = 5; //primitive
var b = a; //b just copies the value of a
a = 6; //This will change a, but not b
conosle.log(b); //will print 5
//Object
var a = { someValue: 5 }; //a is now a reference to that object
var b = a; //b also becomes a reference to the object above
a.someValue = 1337; //will change the object a is referencing, thus also
//changing the object b is referencing, as its the same object
console.log(b.someValue); //will print 1337

Angular - Organise controller, factory and "class"

I would like to understand how to have a nice organisation in my angular project.
[see code below]
Does it makes sense to have the getFireList function into the Factory ? Or should i put it into the controller ?
Does the "class" Fire makes sense ? Should i remove it ? Should i move it to the controller ? Should i move it the the factory ?
If you see anything wrong in this code i'm really interested to learn more.
For now, i've got this :
A class "Fire" to create new object of type Fire.
function Fire (p_power) {
// ATTRIBUTES
this.id = null;
this.power = p_power;
this.position = {
x: null,
y: null
}
// GETTERS/SETTERS
// id
this.getId = function() {
return this.id;
}
this.setId = function(p_id) {
this.id = p_id;
}
// power
this.getPower = function() {
return this.power;
}
this.setPower = function(p_power) {
this.power = p_power;
}
// position
this.getPosition = function() {
return this.position;
}
this.setPosition = function(p_position) {
this.position = p_position;
}
// METHODS
this.increasePower = function(p_plus) {
this.power += p_plus;
}
this.decreasePower = function(p_minus) {
this.power -= p_minus;
}
}
A controller
simuApp.controller('FireController', function($scope, FireFactory) {
// ...
});
And a factory
simuApp.factory('FireFactory', function() {
return {
fire_list: [],
getFireList : function() {
return $http.get(site_url+'fire/fireList').
then(
function(success) {
var data = success.data;
var fires = [];
var fire_tmp;
for (i=0 ; i<data.length ; i++) {
fire_tmp = new Fire( data[i].power );
fire_tmp.setId( data[i].idFire );
fires.push( fire_tmp );
}
fire_list = fires;
return fire_list;
}, function(err) {
// ...
}
);
}
}
});
Thanks for your help.
First, let's get the terminology right. .factory is a method to register a function that generates an instance of the service - hence "factory". What it generates, though, is a singleton service instance.
So, the service you create would be more properly named as FireSvc (as opposed to FireFactory), whereas the function that creates it could have the word "factory" in it (although, in the case below, that function name is not really needed - it could just be an anonymous function):
.factory("FireSvc", function FireSvcFactory(){
});
It is a good practice to use a Service to abstract away any domain/business logic from the controller. Keep the controller thin, responsible only to define the ViewModel, and react to events by changing the ViewModel or invoking functions on the Model.
So, having FireSvc.getFireList() makes sense.
Now, whether the list is a collection of plain objects, or instances of Fire is completely independent of Angular and is entirely up to you. In any case, it is too broad of a topic to discuss in a SO answer.

How to extend returned objects in the list returned by $asArray?

I'm having trouble decorate the objects in my list returned by $asArray in angularfire with a new method (not decorating the array itself).
The angularfire documentation seems to suggest that the right way to do this is to override the $$added method in the factory for $FirebaseArray, returning a new object that either encapsulates or extends the snapshot that gets passed in to that method. From the documentation:
// an object to return in our JokeFactory
app.factory("Joke", function($firebaseUtils) {
function Joke(snapshot) {
this.$id = snapshot.name();
this.update(snapshot);
}
Joke.prototype = {
update: function(snapshot) {
// apply changes to this.data instead of directly on `this`
this.data = snapshot.val();
},
makeJoke: function() {
alert("Why did the " + this.animal + " cross the " + this.obstacle + "?");
},
toJSON: function() {
// since we didn't store our data directly on `this`, we need to return
// it in parsed format. We can use the util function to remove $ variables
// and get it ready to ship
return $firebaseUtils.toJSON(this.data);
}
};
return Joke;
});
app.factory("JokeFactory", function($FirebaseArray, Joke) {
return $FirebaseArray.$extendFactory({
// change the added behavior to return Joke objects
$$added: function(snap) {
return new Joke(snap);
},
// override the update behavior to call Joke.update()
$$updated: function(snap) {
this.$getRecord(snap.name()).update(snap);
}
});
});
However, when I do this in my code, nothing ever gets added to the array, although I can see from outputting to the console that it is getting called.
var printMessageObjConstructor = function(snap) {
this.$id = snap.name();
this.snapshot = snap;
this.$update = function(snap) {
this.snapshot = snap;
};
this.printMessage = function() {
return this.author + "'s question is: " + this.body;
};
};
var ref = new Firebase("https://danculley-test.firebaseio.com/questions");
//What Am I Doing Wrong Here?
var arrayFactory = $FirebaseArray.$extendFactory({
$$added: function(snap, prevChild) {
var x = new printMessageObjConstructor(snap);
console.log("I am being called from FirebaseDecoratedCtlOverloadAddedinNewObj.");
return x;
},
$createObject: function(snap) {
return new printMessageObjConstructor(snap);
},
$$updated: function(snap) {
var i = this.$indexFor(snap.name());
var q = this.$list[i];
q.$update(snap);
}
});
var sync = $firebase(ref, {arrayFactory:arrayFactory});
var list = sync.$asArray();
list.$loaded(function(list) {
$scope.questions = list;
});
I've set up a new plunk stripped down to show the issue with a couple other use cases that I've tried. (The actual method I'm adding is more complex and isn't related to the view, but I wanted to do something simple to reproduce the issue.)
I think the issue is that I don't quite understand what exactly $$added is supposed to return, or what additional behavior beside returning the value to be stored $$added is supposed to have. There also doesn't really seem to be an $$added on the prototype or on $FirebaseArray to call as a super to get the default behavior. Can someone point me in the right direction?
UPDATE
For the benefit of others, after reviewing the like that Kato posted, I was able to solve the issue by adding the following, almost all copied directly from the source except for the commented line below.
$$added: function(snap, prevChild) {
var i = this.$indexFor(snap.name());
if( i === -1 ) {
var rec = snap.val();
if( !angular.isObject(rec) ) {
rec = { $value: rec };
}
rec.$id = snap.name();
rec.$priority = snap.getPriority();
$firebaseUtils.applyDefaults(rec, this.$$defaults);
//This is the line that I added to what I copied from the source
angular.extend(rec, printMessageObj);
this._process('child_added', rec, prevChild);
}
}
For the benefit of others, after reviewing the link that Kato posted, I was able to solve the issue by adding the following, almost all copied directly from the source except for the commented line below.
$$added: function(snap, prevChild) {
var i = this.$indexFor(snap.name());
if( i === -1 ) {
var rec = snap.val();
if( !angular.isObject(rec) ) {
rec = { $value: rec };
}
rec.$id = snap.name();
rec.$priority = snap.getPriority();
$firebaseUtils.applyDefaults(rec, this.$$defaults);
//This is the line that I added to what I copied from the source
angular.extend(rec, printMessageObj);
this._process('child_added', rec, prevChild);
}
}

Backbone Model change data in one instance affects another

I just had a weird bug when using Backbone.Model
so I have the model declaration something like:
var MyMode = Backbone.Model.extend({
defaults:{
'someList': []
},
initialize: function(){
_.bindAll(this, 'toString', 'castFromString');
},
toString: function(){
return this.get('hierarchyNameList').join('+');
},
castFromString: function(str){
var strArray = [], index = 0;
if (str) {
strArray = str.split('+');
}
for (index = 0; index < strArray.length; index++){
this.get('hierarchyNameList').push(strArray[index]);
}
}
});
then I tried to test it
(function () {
'use strict';
var assert = function(condition, message) {
if (condition !== true) {
throw message || "Assertion failed";
}
};
var mA = new MyModel({'someList': ['a','b','c']});
var mB = new MyModel();
mB.castFromString('a+b+c');
//I have a added a equals function to array prototype and tested it
assert(mA.get('someList').equals(mB.get('someList'))); //true
var mC = new MyModel(mA.toJSON()); //behaves as expected
var mD = new MyModel(); //for some reason its list already contains the list from B
mD.castFromString(mB.toString()); //since castFromString used push, now B and D both has array of length 6
assert(mC.equals(mA)); //success
assert(mC.equals(mD)); //fail, mc has arrayLength 3, mD has 6
}).call(this);
The actual code is more complicated than this, but I think this is where I am probably doing something wrong, any suggestion on why this would happen? Thanks in advance!
The problem is with your defaults
defaults:{
'someList': []
},
objects in JavaScript are passed by reference not by value. It means that all instances, for which you didn't explicitly specified someList value will share array created in defaults definition above. To avoid it you can define your defaults as a function:
defaults: function () {
return { 'someList': [] };
},
This will create new array for every instance of MyModel, so they won't share the same array.

Resources