unbinding a view from a model - javascript

This jsfiddle ( http://jsfiddle.net/mjmitche/avo5nnus/39/ ) demonstrates the problem I'm having, but I will explain here. I have a main view that has a startInterval and stopInterval function. In the startInterval, I call a method that adds values to a stats array inside a model that I create in the startInterval function with code this.model = new myModel(). If I stop and then startInterval again (at which point the code runs again this.model = new myModel(), it's still the same array from the first press of the startInterval button.
For example, if I press start, and the random number 3 gets added to the stats array (in the addToModel function), and then press stop, and then press start again which adds a 5 to the stats array, the array will actually have a 3, and a 5. You can see the values print to the screen in the jsfiddle if you press start and stop.
In my real application, I've tried to do things like setting the stats array to [] but i can't clear it. Ideally I wish to dereference the view from the model.
startInterval: function(){
this.model = new myModel();
this.model.intervalId = setInterval(this.addToModel.bind(this), 1000);
},
stopInterval: function(){
clearInterval(this.model.intervalId);
var modelstats = this.model.get("stats");
},
addToModel: function(){
var arr = this.model.get("stats");
var num = Math.floor((Math.random() * 10) + 1);
var view = new StatView({model: this.model});
arr.push(num);
this.model.set({"stats" : arr });
}
Any ideas on a solution?
Although my code doesn't show it, the model is eventually getting added to a collection and then saved to a database, so if the only solution involves destroying the model please take that into consideration (i.e. is there a way to destroy a model without removing it from a collection)

your "stats" attributes is shared on the prototype via defaults.
because in javascript arrays and objects are mutable, it references to the same array and doesn't create a new one.
What you can do instead is have the defaults as a function that returns the default - this way they will be unique per instance
var myModel = Backbone.Model.extend({
defaults: function () {
return {
stats: [],
intervalId: ''
}
}
});

Related

How do I use fetched backbone collection data in another view?

Trying to learn Backbone and hitting a stumbling block when trying to fetch data, I fetch the data fine from with my view SearchBarView but once the data has been fetched I don't know how I can get this data in my SearchResultsView in order to template out each result?
Sorry if this sounds a little vague, struggling to get my head around this at the moment so could do with the guidance!
SearchBarView
performSearch: function(searchTerm) {
// So trim any whitespace to make sure the word being used in the search is totally correct
var search = $.trim(searchTerm);
// Quick check if the search is empty then do nothing
if(search.length <= 0) {
return false;
}
// Make the fetch using our search term
dataStore.videos.getVideos(searchTerm);
},
Goes off to VideoSearchCollection
getVideos: function(searchTerm) {
console.log('Videos:getVideos', searchTerm);
// Update the search term property which will then be updated when the url method is run
// Note make sure any url changes are made BEFORE calling fetch
this.searchTerm = searchTerm;
this.fetch();
},
SearchResultsView
initialize: function() {
// listens to a change in the collection by the sync event and calls the render method
this.listenTo(this.collection, 'sync', this.render);
console.log('This collection should look like this: ', this.collection);
},
render: function() {
var self = this,
gridFragment = this.createItems();
this.$el.html(gridFragment);
return this;
},
createItems: function() {
var self = this,
gridFragment = document.createDocumentFragment();
this.collection.each(function (video) {
var searchResultView = new SearchResultView({
'model': video
});
gridFragment.appendChild(searchResultView.el);
}, this);
return gridFragment;
}
Now I'm not sure how I can get this data within SearchResultView, I think I need to trigger an event from somewhere and listen for the event in the initialize function but I'm not sure where I make this trigger or if the trigger is made automatically.
Solution 1
If dataStore is a global variable then
SearchBarView
dataStore - appears like a global variable
videos - a collection attached to global variable
then in
SearchResultsView
this.listenTo(dataStore.videos, 'sync', this.render);
Solution 2
If dataStore is not a global variable
getVideos: function(searchTerm) {
console.log('Videos:getVideos', searchTerm);
// Update the search term property which will then be updated when the url method is run
// Note make sure any url changes are made BEFORE calling fetch
this.searchTerm = searchTerm;
var coll=this; //this should refer to the collection itself
this.fetch().done(function(){
var searchResultView = new SearchResultsView({collection:coll});
searchResultView.render();
});
},
It is not 100% clear how you are initializing your SearchResultView.
But, in order to have reference to the collection, can't you simply pass in the reference to the constructor of the view. Something like this:
// In your SearchbarView
var myCollection = new Backbone.Collection(); // and you are populating the collection somewhere somehow
var searchResultView = new SearchResultView(myCollection) // you just pass this collection as argument.
myCollection.bind("change", function(){
searchResultView.parentCollection = myCollection;
}
And inside your searchResultView you just refer this collection by parentCollection for instance.
If you make it more explicit as in how these 2 views are connected or related, I may be able to help you more. But, with given info, this seems like the easiest way.

Why does my backbone collection contain an empty model item?

I have a game model with a scorecards attribute that is a collection. I'm nesting this collection so when I initialize I'm using nestCollection to create the change handlers to keep everything updated and in sync. Whenever I create a new game model, an empty model is added to the scorecards attribute collection but only in memory - what is saved to localstorage is correct. I can't figure out why.
This is my game model definition- Notice the console log statement results:
var Game = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage('datastore'),
defaults: {
name : '',
scorecards: new ScorecardList(),
created : 0
},
initialize : function() {
console.log(this.scorecards); // prints undefined
console.log(this.get('scorecards')); // length is 0 as expected
this.scorecards = nestCollection(this, 'scorecards', new ScorecardList(this.get('scorecards')));
console.log(this.scorecards); // length is 1, with empty element in it
console.log(this.get('scorecards')); // length is 0 as expected
if (this.isNew()) this.set('created', Date.now());
}
});
The nesting code:
function nestCollection(model, attributeName, nestedCollection) {
//setup nested references
for (var i = 0; i < nestedCollection.length; i++) {
model.attributes[attributeName][i] = nestedCollection.at(i).attributes;
}
//create empty arrays if none
nestedCollection.bind('add', function (initiative) {
if (!model.get(attributeName)) {
model.attributes[attributeName] = [];
}
model.get(attributeName).push(initiative.attributes);
});
nestedCollection.bind('remove', function (initiative) {
var updateObj = {};
updateObj[attributeName] = _.without(model.get(attributeName), initiative.attributes);
model.set(updateObj);
});
return nestedCollection;
}
This is the code I use to create a new game:
addGame: function () {
var g = new Game({
name:this.ui.gameName.val()
});
app.gameList.create(g,{wait:true});
//Backbone.history.navigate('game/new/'+ g.id, true);
}
Your problem comes from this piece of code:
new ScorecardList(this.get('scorecards'))
Here you're giving your ScorecardList constructor another collection as argument. This collection happens to be an object. So your collection's constructor will think it's an object you're giving it to create a model.
So basically, this.get('scorecards')) gets cast into a Scorecard (or whatever your model is called), and that's why you have an empty model.
Passing arguments to the constructor for a different purpose than the creation of your collection is a bad idea, you should call a method afterwards.

Can't see my model within backbone collection

I'm trying to add an item to a collection but first I want to remove the existing one. Only one item will ever exist. I can create a new one, just not remove one. Maybe I'm doing it backwards.
This is my collection, the changetheme is the function that gets called, which works away, but can't figure out how to remove the existing one. this.model.destroy() just throws an error. Maybe i'm out of context.
bb.model.Settings = Backbone.Collection.extend(_.extend({
model: bb.model.Setting,
localStorage: new Store("rrr"),
initialize: function() {
var self = this
this.model.bind('add', this.added, this);
},
changetheme: function(value) {
var self = this
this.destroy();
this.create({theme:value});
},
}));
If it matters this is my model
bb.model.Setting = Backbone.Model.extend(_.extend({
defaults: {
theme: 'e'
},
initialize: function() {
var self = this;
},
added: function(item) {
var self = this;
this.destroy();
},
}));
To remove first item from collection you can call collection.shift(), also you can just clear collection by calling collection.reset(). So in your case one could write:
changetheme: function(value) {
this.shift();
this.create({theme:value});
}
UPD
Ok, let me explain - in your example localStorage plays like any other server side. So when you call "create", then according to docs backbone instantiates a model with a hash of attributes, saves it to the server(localStorage), and adds to the set after being successfully created. That is why your collection items count increases on each page refresh. But when you call shift/remove docs then only you client side collection is affected, not the server(localStorage) one. Now the best option for you to remove model both from server and client is calling model's destroy method like that:
changetheme: function(value) {
var modelToDelete = this.at(0) //take first model
modelToDelete.destroy();
this.create({theme:value});
}

Backbone.js unique attribute name across all models in collection for permalink

I would like to make sure that a specific attribute is unique across the collection when a new model is created so that I can ultimately use this attribute as a permalink.
I have a List Collection and List Model. (CoffeeScript)
class App.Models.List extends Backbone.Model
defaults:
name: "My New List"
class App.Collections.ListSet extends Backbone.Model
model: App.Models.List
When a new model is added to the ListSet, I would like to add a auto-incrementing number to the end of the name attribute if a model exists with that name already. For example My New List 1 if a model existed with My New List.
I don't want it to be obtrusive and show an error, but just fix the problem automatically. I am happy with the logic behind adding the incrementing integer, but I need direction on where to put the code to make the changes.
This might work for you:
App.Models.List = Backbone.Model.extend({
initialize: function(attrs) {
this.set(attrs);
this.set({originalName: attrs.name});
var count = yourCollection.reduce(function(count,model){
return count + (model.get('originalName') === this.get('originalName') ? 1 : 0);
},0,this);
if(count > 0) {
this.set({name: this.get('name') + ' ' + count});
}
}
});
Here's a demo: jsFiddle DEMO
What this should do is every time you initialize a model, you scan your collection to see if any other models have that same originalName. If so, you tack on the result to the name attribute.
=== UPDATE ===
I realize now that it's probably better to do the modification when the model is added to the collection, so as to decouple the model from the collection. Here's an updated code snippet:
App.Models.List = Backbone.Model.extend({
initialize: function(attrs) {
this.set({originalName: attrs.name});
}
});
App.Collections.Lists = Backbone.Collection.extend({
model: App.Models.List,
initialize: function() {
this.on('add',this.resolveNameConflict,this);
},
resolveNameConflict: function(model){
var count = this.reduce(function(c,m){
return c + (m.get('originalName') === model.get('originalName') ? 1 : 0);
},0);
if(count > 1) {
model.set({name: model.get('originalName') + ' ' + (count-1)});
}
}
});
and a demo: jsFiddle DEMO
UPDATE: jackwander's answer is a better solution...
I'm going to keep this here though because it's interesting to compare and contrast my naive solution to his. Beginner programmers, you can learn something by evaluating the two. :-)
I'll take a stab at it. You might do something like this. Basically we add an eventListener that does a name check every time you add a model to the collection. The nameTracking array is a sort of catalogue of name objects that have a unique name fragment and the counter which keeps track of what the last count of the name root is. Useful for our incrementing application.
ListSet = Backbone.Collection.extend({
initialize: function() {
this.nameTracking = [];
this.on('add', this.pushModelName, this);
},
pushModelName: function(model, collection) {
// First, look through current collection for a model with same name
var duplicate = this.find(function(listModel) {
return model.get('name') === listModel.get('name');
});
// If duplicate exists, we lookup the name and counter in the nameTracking array.
if (duplicate) {
var nameObj = _.find(this.nameTracking, function(nameObj) {
return nameObj.name === model.get('name');
});
// We increment the counter so that our added model is appended with the
// correct, "latest" increment/indicator
nameObj.count = nameObj.count++;
model.set('name', model.get('name') + '_' + nameObj.count);
// Otherwise it doesn't exist so we push the name into our nameTracking array
} else { // Not a duplicate, new name
var modelName = model.get('name');
this.nameTracking.push({
'name': modelName,
'count': 0
});
}
}
});
Thing to note is that this assumes the model you're adding is a new model and not one that already has an id of a model that already exists in collection (not a clone.) You'd also want some way to update the nameTracking[] on collection initialization or fetch() because this assumes you're starting from scratch. I don't deal with removal of models and updating the name counter stuff but based on this sketch it shouldn't be hard to implement if you desire something of that nature.

javascript item splice self out of list

If I have an array of objects is there any way possible for the item to splice itself out of the array that contains it?
For example: If a bad guy dies he will splice himself out of the array of active enemies.
I probably sound crazy but that ability would simplify my code dramatically, so I hope for something cool =)
The way you would do it is as follows:
var game_state = { active_enemies: [] };
function Enemy() {
// Various enemy-specific things go here
}
Enemy.prototype.remove = function() {
// NOTE: indexOf is not supported in all browsers (IE < 8 most importantly)
// You will probably either want to use a shim like es5-shim.js
// or a utility belt like Underscore.js
var i = game_state.active_enemies.indexOf(this);
game_state.active_enemies.splice(i, 1);
}
See:
Es5-Shim
Underscore.js
Notta bene: There are a couple of issues here with this manner of handling game state. Make sure you are consistent (i.e. don't have enemies remove themselves from the list of active enemies, but heroes remove enemies from the map). It will also make things more difficult to comprehend as the code gets more complex (your Enemy not only is an in-game enemy, but also a map state manager, but it's probably not the only map state manager. When you want to make changes to how you manage map state, you want to make sure that code is structured in such a way that you only need to change it in one place [preferably]).
Assuming the bad guy knows what list he's in, why not?
BadGuy.prototype.die = function()
{
activeEnemies.splice(activeEnemies.indexOf(this), 1);
}
By the way, for older browsers to use indexOf on Arrays, you'll need to add it manually.
You kind of want to avoid circular references
I would suggest creating an object/class that represents the active enemies list. Create methods on that instance for adding/removing a given item from the list - abstracting the inner workings of the data structure from the outside world. If the active enemies list is global (e.g. there's only one of them), then you can just reference it directly to call the remove function when you die. If it's not global, then you'll have to give each item a reference to the list so it can call the function to remove itself.
You can also use an object and instead of splice, delete the enemy:
var activeEnemies = {};
function Enemy() {
this.id = Enemy.getId(); // function to return unique id
activeEnemies[this.id] = this;
// ....
}
Enemy.getId = (function() {
var count = 0;
return function() {
return 'enemyNumber' + count++;
}
}());
Enemy.prototype.exterminate = function() {
// do tidy up
delete activeEnemies[this.id];
}
Enemy.prototype.showId = function() {
console.log(this.id);
}
Enemy.prototype.showEnemies = function() {
var enemyList = [];
for (var enemy in activeEnemies) {
if (activeEnemies.hasOwnProperty(enemy)) {
enemyList.push(enemy);
}
}
return enemyList.join('\n');
}
var e0 = new Enemy();
var e1 = new Enemy();
console.log( Enemy.prototype.showEnemies() ); // enemyNumber0
// enemyNumber1
e0.exterminate();
console.log( Enemy.prototype.showEnemies() ); // enemyNumber1

Categories

Resources