Backbone infinite loop when creating models - javascript

I'm doing something pretty standard, I think.
Model:
app.model.Todo = Backbone.Model.extend({
defaults: {
task: ''
, completed: 0
, attachments: []
, note: ''
}
});
Collection:
var Todos = Backbone.Collection.extend({
model: app.model.Todo
, localStorage: new Store('Todos')
, incomplete: function () {
return this.filter(function (todo) {
return !todo.get('completed')
});
}
, complete: function () {
return this.filter(function (todo) {
return todo.get('completed')
});
}
, comparator: function(todo) {
return todo.get('order');
}
});
app.collection.Todos = new Todos();
Then, if I just do:
app.collection.Todos.create({task: 'hi'});
app.collection.Todos.create({task: 'hi'});
The 2nd one never works. I get an infinite loop (too much recursion on Firefox and stack_overflow on Chrome).
I'm really at a loss. I commented out all events as well.
Appears it spins out of control here in backbone:
// Return a copy of the model's `attributes` object.
toJSON: function(options) {
return _.clone(this.attributes);
},
UPDATE: If I add id: 0 or whatever id to the model the error stops, but if I give it a custom ID (i.e. new Date().getTime() the error happens again. It's like whenever I create a unique item it blows up.
UPDATE 2:
var todo = new gator.model.Todo({task: actionbarVal});
gator.collection.Todos.add(todo);
gator.collection.Todos.sync('create', todo);
Doing the above kinda works, and for what I need it for it works, but it's really bad. It's bad because every single time we do a new add and sync it calls toJSON 1 time for every time add and sync has been called on this page load. So, if you add 3 items, you get 6 toJSON calls (1 for the first, 2 for the second, 3 for the third). Also, it's not as clean. I also noticed in the toJSON call in backbone this.attributes with create was correct the first time. The 2nd time it was like this.attributes == backbone or something. Very, very strange. It had all the methods of Backbone. It was as if clone did a deep clone or something.

You have a mismatch between your version of Backbone (v0.9.9) and the version of the localstorage add-on. Be sure to get the latest version of the localstorage add-on from the Backbone repo and it will fix this problem.

I eventually fixed it by reverting back to 0.9.2 of Backbone, thanks to Derick Bailey. My attempts of using the latest localStorage add-on didn't seem to fix it. Maybe I was using a different source? I was using develop of this:
https://github.com/jeromegn/Backbone.localStorage

Related

Truly protecting data members (of classes/IIFE variables) in JavaScript

Context
I have been tasked with fixing a big bug on the menu-edit page, which was caused by a stale element issue, caused by the HTML elements for it being rendered server-side. In my three-day fight against this bug, I got some inspiration from Angular and decided to try to make a menu state that will power everything on the page (adding/removing categories/items, and later, pagination of the modals for the adding)
Some Code
I came up with this IIFE (to be the "controller" of the MVC. Selector modals hit the add methods of this, and delete buttons hit the remove methods of this. Also, this gets passed to template-render function, which is literally the first thing hit when a modal gets popped):
/* all the categories, items, and modifiers that power this page */
var menuState = (function() {
let _categories = {
attached: [],
available: []
}, _items = {
attached: [],
available: []
}, _modifiers = {
attached: [],
available: []
}
function getExposedMethodsFor(obj) {
return {
all : function() { return obj.attached.concat(obj.available) },
attached : function() { return obj.attached },
available : function() { return obj.available }
// ... other methods that work on obj.attached,obj.available
}
}
let categoryExposedMethods = getExposedMethodsFor(_categories)
// other exposer objects
return {
getAllCategories : categoryExposedMethods.all,
getAttachedCategories : categoryExposedMethods.attached,
getAvailableCategories : categoryExposedMethods.available
// the rest of the exposed methods irrelevant to this question at hand
}
})()
OK, so what's the problem?
The problem is that this is false sense of security, it seems. When I try to XSS-test this structure alone, it fails.
I test it with three entities in _categories, all of which are attached, causing
menuState.getAllCategories().length
to return 3 and
menuState.getAvailableCategories().length
to return 0. Good news is that when I tried
menuState.getAllCategories().push('a')
menuState.getAllCategories().length
I still get three.
However, when I go
menuState.getAvailableCategories().push('b')
menuState.getAvailableCategories().length
I get 1, instead of 0 !!
Is there truly a way to lock down the other getters here?! If not, what are my alternatives?
I fixed it with Object.freeze, which I already used for refactoring the "enums" the dev before me wrote when he was working on this project. What it does is fully protect a state from any type of changes, including:
adding properties
deleting properties
modifying properties
re-assigning the object/array being "frozen"
How I use it
In the helper method, I did the following :
attached : function() { return Object.freeze(obj.attached) },
available : function() { return Object.freeze(obj.available) },
This prevents the arrays being changed from those methods, thus shutting down this type of XSS. Also, menuState was declared with const.

Backbone subview not rendered properly

I started developping a website using backbone.js and after trying during the whole morning, i'm quite stuck on the following problem.
I output here only the relevant code.
I've a View called Navigator, that contains a Collection of Records (initially empty) :
var NavigatorView = Backbone.View.extend({
template: JST['app/scripts/templates/Navigator.ejs'],
tagName: 'div',
id: '',
className: 'saiNavigator',
events: {},
initialize: function () {
this.currentRecords = new RecordsCollection();
this.currentRecords.on('reset', this.onRecordsCollectionReseted.bind(this));
},
onRecordsCollectionReseted: function(){
this.render();
},
render: function () {
var tplResult = this.template({
computeTemplate: this.computeTemplate,
records: this.currentRecords
});
this.$el.html(tplResult);
},
onDOMUpdated: function(){
var me = this;
var data = {
device : 'web',
gridId : this.model.get('gridId'),
filterId : this.model.get('filterId')
};
$.ajax({
url: App.getTokenedUrl() + '/task/getGridData.'+this.model.get('taskId')+'.action',
success: me.onRecordReceived.bind(me),
statusCode: {
500: App.handleInternalError
},
type: 'GET',
crossDomain: true,
data : data,
dataType: 'json'
});
},
onRecordReceived: function(result){
var newRecords = [];
for(var i = 0; i < result.items.length; i++){
var newRecord = new RecordModel(result.items[i]);
newRecords.push(newRecord);
}
this.currentRecords.reset(newRecords);
}
});
I've a View called dossier which html is
<div id="dossier1" class="dossier">
<div id="dossier1-navContainer" class="navigatorContainer"/>
<div class="pagesNavigatorContainer"/>
<div class="pagesContainer"/>
<div class="readOnlyFiche"/>
</div>
When i first render the dossier (and i render it only once) i create the navigator in the following render function
render: function () {
this.$el.html(this.template({
uniqBaseId: this.id,
className: this.className
}));
var nav = this.navigator = new NavigatorView({
model : this.model,
id: this.id+'navigator',
el: $('#'+this.id+'-navContainer')
});
this.navigator.render();
//We notify the navigator that it's ready. This will allow the nav to load records
nav.onDOMUpdated();
}
}
As we can see, i give the '#dossier1-navContainer' id to the navigator so that he renders there
So, here is how it works. When i render the dossier, it creates a navigator and inserts it in the DOM. When done, i notify the navigator that it can load its data from the server trough ajax request. When i receive the answer i reset the collection of data with the incoming record.
Juste before the this.$el.html(tplResult) in the navigator render function i output the resulting string.
First time it's
<div class="items"></div>
Second time when i get records, it's
<div class="items">
<div>item1</div>
<div>item2</div>
<div>item3</div>
</div>
So the template generation is correct. However, when the second rendering occurs, the this.$el.html(tplResult) does NOTHING. If i look at the DOM in the browser NOTHING CHANGED
However if i replace this line by
$('#dossier1-navigator').html(tplResult)
it works. Which means that the first time, $('#dossier1-navigator') and this.$el are the same object, the second time not.
I've NO idea why it doesn't work the second time with the standard this.$el.
Help!!
Thanks in advance
Edit : after discussing a lot with Seebiscuit, i'm adding the few lines that helped answering the question
newTask.render();
var taskHtml = newTask.$el.html();
$('#mainTaskContainer').append(taskHtml);
My hunch is that your having a binding problem. I would suggest that you replace
this.currentRecords.on('reset', this.onRecordsCollectionReseted.bind(this)); },
in your initialize, with:
this.listenTo(this.currentRecords, "reset", this.render);
No need to specially bind. Backbone's listenTo bids the callback to the Backbone object that sets the listener (the this in this.listenTo). Also has the added benefit that when you close the view (by calling this.remove()) it'll remove the listener, and help you avoid zombie views.
Try it out.
I think the problem is that you are not using what your are passing to your navigatorView;
In your navigatorView try this:
initialize:function(el) {
this.$el=el
...
}
Let me know if it helps
After countless minutes of discussion with seebiscuit, we came up with the solution. The problem is all on the definition of the $el element. The formal definition defines it as
A cached jQuery object for the view's element. A handy reference instead of re-wrapping the DOM element all the time
This is actually not very exact from a standard cache point of view. From my point of view at least the principle of a cache is to look for the value if it doesn't have it, and use it otherwise. However in this case this is NOT the case. As Seebiscuit told me,
Because when you first bound this.$el = $(someelement) this.$el will always refer to the return of $(someelement) and not to $(someelement). When does the difference matter?
When the element is not in the DOM when you do the assignment
So actually, $el holds the result of the first lookup of the selector. Thus, if the first lookup misses then it won't succeed ever! Even if the element is added later.
My mistake here is to add the main dossierView into the DOM after rendering its NavigatorView subview. I could have found the solution if the $el was a real cache as the 2nd rendering in the ajax callback would have found the element. With the current way $el works i had just nothing.
Conclusion : make sure every part of your view is properly rendered in the DOM at the moment your try to render a subview.

knockoutjs - ko.mapping.fromJS not working

I have just started trying knockout.js. The ko.mapping offers a nifty way to get and map data from server. However I am unable to get the mapping to work.
I have a simple model:
//var helloWorldModel;
var helloWorldModel = {
name: ko.observable('Default Name'),
message: ko.observable('Hello World Default')
};
$(document).ready(function() {
ko.applyBindings(helloWorldModel);
//a button on the form when clicked calls a server class
//to get json output
$('#CallServerButton').click(getDataFromServer);
});
function getDataFromServer() {
$.getJSON("HelloSpring/SayJsonHello/chicken.json", function(data) {
mapServerData(data);
});
}
function mapServerData(serverData) {
helloWorldModel = ko.mapping.fromJS(serverData, helloWorldModel);
alert(JSON.stringify(serverData));
}
The helloWorldModel has only 2 attributes - exactly the same thing I return from the server. The alert in mapServerData shows -
{"name":"chicken","message":"JSON hello world"}
I have looked up other posts regarding similar problem, but none of them seemed to be solve this issue. Maybe I am missing something very basic - wondering if anyone can point it out.
Also note if I do not declare the model upfront and use
helloWorldModel = ko.mapping.fromJS(serverData);
it is mapping the data to my form correctly.
From Richard's reply and then a little more investigation into this I think that the way I was initializing the model is incorrect. I guess that one cannot use an existing view model and then expect it to work with mapper plugin. So instead you initialize view model with raw JSON data using the ko.mapping.fromJS:
var helloWorldModel;
$(document).ready(function() {
var defaultData = {
name: 'Default Name',
message: 'Hello World Default'
};
helloWorldModel = ko.mapping.fromJS(defaultData);
ko.applyBindings(helloWorldModel);
$('#CallServerButton').click(getDataFromServer);
});
function getDataFromServer() {
$.getJSON("HelloSpring/SayJsonHello/chicken.json", function(data) {
mapServerData(data);
});
}
function mapServerData(serverData) {
alert(JSON.stringify(serverData));
ko.mapping.fromJS(serverData, helloWorldModel);
}
This code works and provides the expected behavior
You can't just overwrite your model by reassigning it this way.
When you do:
ko.applyBindings(helloWorldModel);
You are saying "bind the model helloWorldModel to the page". Knockout then goes through and hooks up the observables in that model and binds them with the page.
Now when you overwrite your form model here:
helloWorldModel = ko.mapping.fromJS(serverData, helloWorldModel);
It is overwriting your model object with a brand new object with entirely new observables in it.
To fix it you need to change this line to just:
ko.mapping.fromJS(serverData, helloWorldModel);
This takes care of the properties inside the model and reassigns them for you, without overwriting your model.

Sencha Touch 2.2 List not reusing list-items

I have a list linked to a store filled with Facebook friends. It contains around 350 records.
There is a searchfield at the top of the list which triggers the following function on keyup:
filterList: function (value) {
// console.time(value);
if (value === null) return;
var searchRegExp = new RegExp(value, 'g' + 'i'),
all = Ext.getStore('Friends'),
recent = Ext.getStore('Recent'),
myFilter;
all.clearFilter();
recent.clearFilter();
// console.log(value, searchRegExp);
myFilter = function (record) {
return searchRegExp.test(record.get('name'));
}
all.filter(myFilter);
recent.filter(myFilter);
// console.timeEnd(value);
},
Now, this used to work fine with ST2.1.1 but since I upgraded the app to ST2.2. It's really slow. It even makes Safari freeze and crash on iOS...
This is what the logs give :
t /t/gi Friends.js:147
t: 457ms Friends.js:155
ti /ti/gi Friends.js:147
ti: 6329ms Friends.js:155
tit /tit/gi Friends.js:147
tit: 7389ms Friends.js:155
tito /tito/gi Friends.js:147
tito: 7137ms
Does anyone know why it behaves like this now, or does anyone have a better filter method.
Update
Calling clearFilter with a true paramater seems to speed up things, but it's not as fast as before yet.
Update
It actually has nothing to do with filtering the store.
It has to do with rendering list-items. Sencha apparently create a list-item for every record I have in the store instead of just creating a couple of list-items and reusing them
Could there be an obvious reason it's behaving this way ?
Do you have the "infinite" config on your list set to true?
http://docs.sencha.com/touch/2.2.0/#!/api/Ext.dataview.List-cfg-infinite
You probably don't want the list rendering 300+ rows at once, so setting that flag will reduce the amount of DOM that gets generated.

Getting backbone.js to run a function after constructing a Collection?

I may be completely missing something here, but I have the following:
a Model which encapsulates 'all' the data (all JSON loaded from one URL)
the model has one (or more) Collections which it is instantiating with the data it got on construction
some code which I want to run on the Collection when the data is initialized and loaded
My question is about the composed Collection. I could do this outside the scope of the Collection, but I'd rather encapsulate it (otherwise what's the point of making it a 'class' with an initializer etc).
I thought I could put that code in the initialize() function, but that runs before the model has been populated, so I don't have access to the models that comprise the collection (this.models is empty).
Then I thought I could bind to an event, but no events are triggered after initialization. They would be if I loaded the Collection with a fetch from its own endpoint, but I'm not doing that, I'm initializing the collection from pre-existing data.
My question: How to get initialize code to run on the Collection immediately after it is initialized with data (i.e. this.models isn't empty).
Is it possible to do this without having to get 'external' code involved?
Okay here is the demo code, perhaps this will explain things better.
var Everything = Backbone.Model.extend({
url: "/static/data/mydata.json",
parse: function(data)
{
this.set("things", new Things(data.things, {controller: this}));
}
});
var Thing = Backbone.Model.extend({
});
var Things = Backbone.Collection.extend({
model: Thing,
initialize: function(data, options)
{
// HERE I want access to this.models.
// Unfortunately it has not yet been populated.
console.log("initialize");
console.log(this.models);
// result: []
// And this event never gets triggered either!
this.on("all", function(eventType)
{
console.log("Some kind of event happend!", eventType);
});
}
});
var everything = new Everything();
everything.fetch();
// Some manual poking to prove that the demo code above works:
// Run after everything has happened, to prove collection does get created with data
setTimeout(function(){console.log("outside data", everything.get("things").models);}, 1000);
// This has the expected result, prints a load of models.
// Prove that the event hander works.
setTimeout(function(){console.log("outside trigger", everything.get("things").trigger("change"));}, 1000);
// This triggers the event callback.
Unfortunately for you the collection gets set with data only after it was properly initialized first and models are reset using silent: true flag which means the event won't trigger.
If you really wanted to use it you can cheat it a bit by delaying execution of whatever you want to do to next browser event loop using setTimeout(..., 0) or the underscore defer method.
initialize: function(data, options) {
_.defer(_.bind(this.doSomething, this));
},
doSomething: function() {
// now the models are going to be available
}
Digging this an old question. I had a similar problem, and got some help to create this solution:
By extending the set function we can know when the collection's data has been converted to real models. (Set gets called from .add and .reset, which means it is called during the core function instantiating the Collection class AND from fetch, regardless of reset or set in the fetch options. A dive into the backbone annotated source and following the function flow helped here)
This way we can have control over when / how we get notified without hacking the execution flow.
var MyCollection = Backbone.Collection.extend({
url: "http://private-a2993-test958.apiary-mock.com/notes",
initialize: function () {
this.listenToOnce(this, 'set', this.onInitialized)
},
onInitialized:function(){
console.log("collection models have been initialized:",this.models )
},
set: function(models,options){
Backbone.Collection.prototype.set.call(this, models, options);
this.trigger("set");
}
})
//Works with Fetch!
var fetchCollection= new MyCollection()
fetchCollection.fetch();
//Works with initializing data
var colData = new MyCollection([
{id:5, name:'five'},
{id:6, name:'six'},
{id:7, name:'seven'},
{id:8, name:'eight'}
])
//doesn't trigger the initialized function
colData.add(new Backbone.Model({id:9,name:'nine'};
Note: If we dont use .listenToOnce, then we will also get onInitialized called every time a model is added to or changed in the collection as well.

Categories