Backbone Model change data in one instance affects another - backbone.js

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.

Related

JavaScript array default assignment syntax confusion

If I create a function constructor in JavaScript like so:
function Emitter() {
this.events={}
}
and then I add a method to its prototype like so:
Emitter.prototype.on = function(type,listener) {
this.event[type] = this.events[type] | [];
this.event[type].push(listener);
}
When I call the method 'on' an instance of Emitter twice, why does it not just overwrite the original property called greet and assign it the second function? I guess I do not understand the stricture of what is happening in:
this.event[type] = this.events[type] | [];
var emtr = new Emitter();
emtr.on('greet',function(){
console.log('Hello once');
});
emtr.on('greet', function(){
console.log('Hello twice');
});
You should be using || for OR instead of |, this is invalid:
this.event[type] = this.events[type] | [];
Also, you are calling event instead of events. You should have:
this.events[type] = this.events[type] || [];
This way if this.events[type] is not undefined it will stay as is. However if it is undefined is will be assigned as an empty array: []
The code below will successfully add the two functions to emtr.events['greet'] (an array of functions):
function Emitter() {
this.events = {}
}
Emitter.prototype.on = function(type, listener) {
this.events[type] = this.events[type] || [];
this.events[type].push(listener);
}
var emtr = new Emitter();
emtr.on('greet', function() {
console.log('Hello once');
});
emtr.on('greet', function(){
console.log('Hello twice');
});
console.log(emtr.events['greet'])
So you can call them like so:
emtr.events['greet'][0]();
And
emtr.events['greet'][1]()
If instead, you would like to replace the listener then you shouldn't be using an array. Pointing to a function instead of an array of functions will suffice:
function Emitter() {
this.events = {}
}
Emitter.prototype.on = function(type, listener) {
this.events[type] = listener;
}
var emtr = new Emitter();
emtr.on('greet', function() {
console.log('Hello once');
});
emtr.on('greet', function(){
console.log('Hello twice');
});
emtr.events['greet'](); // notice how the first listener was replaced by the new one
This way you can call your listener with emtr.events['greet']().
While #Ivan is completely correct, he missed your main question of “why?”
The answer is that the logical OR operator in JS also functions as a null coalescing operator when used as part of an assignment statement.
Essentially, in the case
let x = y || “default”;
The right hand side will evaluate to the first “truthy” value and return that. In JS most things evaluate true in a logical operation except values like false, 0, null, and a few others not relevant to this question.
So in your syntax (as corrected by Ivan), you’re telling JS to assign to the events[type] property the first thing that’s true. Either itself (which will evaluate true if it’s not null, unassigned, 0, etc) or else an empty array.
The first time you add an event, it’ll be unassigned and therefore get an empty array added. Subsequently, arrays eval as true, so you’ll just keep re-assigning the property to itself which has no effect.
Make sense?
function Emitter() {
this.events = []
}
Emitter.prototype.on = function (type, listener) {
this.events[type] = this.events[type]||[]
this.events[type].push(listener);
}
Emitter.prototype.emit = function(type){
var listener = this.events[type].pop()
if(this.events[type].length>=0)
listener()
else
console.log('Nothing to emit')
}
var emtr = new Emitter();
emtr.on('greet',function(){
console.log('Hello once');
});
emtr.emit('greet')

Return value from angularjs factory

I try to set up this example https://github.com/AngularClass/angular-websocket#usage
Here is my code
App.factory('MyData', function($websocket, $q) {
var dataStream = $websocket('wss://url');
var collection = [];
dataStream.onMessage(function(message) {
var result = JSON.parse(message.data);
console.log(result);
collection = result;
});
var methods = {
collection: collection,
get: function() {
dataStream.send(JSON.stringify({
api: "volume",
date: "2017-02-01",
interval: 600
}));
}
};
return methods; });
In my controller I wrote:
$interval(function () {
console.log(MyData.collection);
}, 1000);
The problem is that I don't receive any values, however on message arrive I see console log, so websocket itself is obviously alive. If I change collection.push(result) (like in example) I receive constantly growing array. I need only the last value, however. Why collection = result is wrong ?
var collection = []; instantiates a new array and its reference is stored in the variable collection. Then, this reference is assigned to methods.collection and, hence, MyData.collection. However, with JSON.parse a new array is instantiated. collection = result; overwrites the original reference with the reference of the new array. But MyData.collection still holds the reference to original array.
So, there are two ways to encounter the problem:
Don't overwrite the reference to the original array. push is good, but before, you need to clear the array in order to only show the last value.
collection.splice(0, collection.length);
collection.push(result);
However, that would be an array in an array. You probably need to push the values individually (Array.concat will create a new array, too):
collection.splice(0, collection.length);
result.forEach(function(value) {
collection.push(value);
});
Assign the reference of the new array directly to methods.collection. In this case, no extra variable collection is needed.
App.factory('MyData', function($websocket, $q) {
var dataStream = $websocket('wss://url');
var methods = {
collection: [],
get: function() {
dataStream.send(JSON.stringify({
api: "volume",
date: "2017-02-01",
interval: 600
}));
}
};
dataStream.onMessage(function(message) {
var result = JSON.parse(message.data);
console.log(result);
methods.collection = result;
});
return methods;
});

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

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);
}
}

Check input value against an array

I am using this plugin for my autocomplete form:
http://www.planbox.com/blog/news/updates/jquery-autocomplete-plugin-for-backbone-js.html
Instead of checking only one item, as in the code below (if (inputVal == 'bakaxel')),
I would like to check the selected value against the entire collection
var collection = new Backbone.Collection([
{id:"AB", name:"Alberta"},
{id:"AD", name:"Album"},
{id:"BA", name:"barn"},
{id:"BC", name:"bak"},
{id:"BD", name:"baby"},
{id:"BE", name:"band"},
{id:"BF", name:"bakaxel"},
{id:"BG", name:"batteri"},
{id:"BH", name:"barbie"},
{id:"MB", name:"Manitoba"},
{id:"AP", name:"Armed Forces Pacific"}
]);
$('input.search').autocomplete({
collection: collection,
attr: 'name',
noCase: true,
ul_class: 'search_options tr_list',
ul_css: {'z-index':1234}
});
$('input.search').each(function(){
$(this).blur(function(){
var inputVal = $('input.search').val();
if (inputVal == 'bakaxel') {
$('#search_result_page').load('searchResult.html');
$('#searchPage').addClass('hidden');
}
});
});
I tried this, but I'd rather not create the ar array again, just use the backbone collection:
$('input.search').each(function(){
$(this).blur(function(){
var inputVal = $('input.search').val();
var ar = ["Alberta", "Album", "barn", "bak", "baby", "band", "bakaxel", "batteri", "barbie", "Manitoba", "Armed Forces Pacific"];
if (jQuery.inArray(inputVal, ar) != -1) {
$('#search_result_page').load('searchResult.html');
$('#searchPage').addClass('hidden');
}
});
});
Backbone proxies Underscore functions and most notably in your case http://underscorejs.org/#where
where _.where(list, properties)
Looks through each value in the list, returning an array of all the values that contain all of the
key-value pairs listed in properties.
Your test could be written as
var matches = collection.where({
name: inputVal
});
if (matches.length>0) {
...
}
Or as #mu suggested in the comments, you could just check the existence of the input with http://underscorejs.org/#find
var found = collection.find(function(model) {
return model.get('name') === inputVal
});
if (found) {
...
}

Resources