Backbone subview not rendered properly - javascript

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.

Related

How can I set observable routes where the content comes from a template when using knockout.js and Sammy.js

I am trying to make a single page application using Sammy.js for routes.
This is the JS:
var galleryTemplateUrl = "templates/galleryTemplate.html";
var registerTemplateUrl = "templates/registerTemplate.html";
var containerId = '#container';
var containerElement = $(containerId);
var app = new Sammy(containerId, function() {
var self = this;
self.get('#/Gallery', function() {
containerElement.html("");
$.ajax({
method: "POST",
url: galleryTemplateUrl
}).done(function (result) {
containerElement.append(result);
ko.applyBindings(testView(),containerElement[0]);
})
});
self.get('#/Register', function(){
containerElement.html("");
$.ajax({
method: "POST",
url: registerTemplateUrl
}).done(function (result) {
containerElement.append(result);
})
});
});
app.run('#/Gallery');
The rest is pretty straightforward. I have a container div in my HTML and the templates are HTML files with some data-bindings in them.
PROBLEM: Once I launch the application the bindings are successfully read the first time I load them however if I change to the next route and come back to the previous route the bindings no longer apply.
I think I'm supposed to reset the function in the ko.applyBinding but I have no idea how.
I've tried adding ko.clearNode() before I make a new AJAX request however that didn't work as well.
Any ideas?
Edit: I found a way around my problem but it's probably not the "right" solution. Basically I put my container div in another div. Then every time I added a template to my container div I deleted it and remade it with a different id.
Edit: The root of the problem seems to be that if cleanNode() is used on the same element which is passed to new Sammy then cleanNode will shut down Sammy as well.Basically don't use cleanNode on the element Sammy is running.
I'd recommend you use Knockout to manage the DOM. That probably means making a component (or maybe just a template) for each route-page, and having the router activate the appropriate one.
This answer may be helpful.

How to only update a Backbone view if the collection changed on the server?

I've got a page for which I poll for notifications. If there are any notifications I populate a Backbone.js view with the notifications. I currently poll for notifications every 2 seconds and simply repopulate the view every two seconds.
function updateNotificationView(ticketId) {
var ticketNotificationCollection = new TicketNotificationCollection([], {id: ticketId});
$.when(ticketNotificationCollection.fetch()).done(function() {
var notificationView = new NotificationsView({collection: ticketNotificationCollection});
$('#ticket-notifications-area').html(notificationView.render().el);
});
}
window.notificationsIntervalId = setInterval(function() {updateNotificationView(ticketId);}, 2000);
I now want to only populate the view if the Backbone fetched collection has changed, but I have no idea how I could do that?
Could anybody give me a tip on how I could only populate the view on collection change? All tips are welcome!
[EDIT]
I now changed the function to this:
function updateNotificationView(ticketId) {
var ticketNotificationCollection = new TicketNotificationCollection([], {id: ticketId});
var notificationsView = new NotificationsView();
notificationsView.listenTo(ticketNotificationCollection, 'change', notificationsView.render);
notificationsView.listenTo(ticketNotificationCollection, 'add', notificationsView.render);
ticketNotificationCollection.fetch();
}
and I changed the NotificationsView to this:
var NotificationsView = Backbone.View.extend({
addNotificationView: function(notification) {
var singleNotificationView = new SingleNotificationView({model: notification});
this.$el.append(singleNotificationView.render().el);
},
render: function() {
console.log('ITS BEING CALLED!!');
this.collection.each(this.addNotificationView, this);
$('#ticket-notifications-area').html(this);
}
});
I now get the "ITS BEING CALLED!!" in the console every two seconds, but also an error:
"Uncaught TypeError: Cannot read property 'each' of undefined.". I get why this error occurs; its because the collection is never actually inserted into the view. I don't get however, how I can solve this.. Any more tips?
I am assuming that you are getting models in the collection in response to your request as JSON.
Method 1:
You can simply set the collection to the JSON. and bind the collection to change event. In case of any change in the collection, callback function will be triggered. In the callback function you can render your view.
Note: change event on collection will be triggered only if there is any change in the models it contain.
e.g.
someCollection.on('change', function(){
someView.render()
});
Method 2:
You can use Backbone's listenTo method, to render the view on change of collection.
someView.listenTo(someCollection, 'change', someView.render);
Hope it helps...

Backbone variables versus page containers . . .?

So I have inherited a bit of backbone.js code and need to make a change to it today. The guy who wrote the original code is on vacation. I am just barely studying up on backbone.js and am pretty much a backbone newbie.
The code below works and does what it was designed for. There is only one issue: The contents of the template file (see below) get rendered into a specific HTML page.
My problem is that I don't fully understand the flow of the code to make an educated guess as far as how and where to insert a reference to an actual container on that HTML page, and get the content to display inside that container.
The class name of the container where I need the output from this function to go is .mngmnt-main-sctn. Is this possible to do?
.
window.ManagementInstancesBackupView = ManagementView.extend({
events: _.extend({
}, ManagementView.prototype.events
),
initialize: function() {
this.model = this.options.model
this.collection = this.options.collection
this.template = _.template($('#instances-management-backup-template').html())
},
render: function() {
var instances = this.collection
// Append container and title
var $el = this.$el.html(this.template({}))
instances.each(function(instance) {
// THIS IS THE CONTAINER THAT SHOULD GET STUFF APPENDED TO:
// $(".mngmnt-main-sctn")
$el.append(this.renderParent(instance));
instance.get('nic').each(function(nic) {
$el.append(this.renderChild(nic));
}, this)
}, this)
return this
},
renderParent: function(instance) {
return new ManagementInstancesBackupParentView({model: instance}).render().$el
},
renderChild: function(nic) {
return new ManagementInstancesBackupChildView({model: nic}).render().$el
}
});
I believe what you are asking is possible like this.
window.ManagementInstancesBackupView = ManagementView.extend({
el: ".mngmnt-main-sctn"
[...code excluded...]
});
We are overriding the el property meaning that when this line is called
var $el = this.$el.html(this.template({}))
this.$el will refer to the element you have specified.
Jacob, thanks again for looking into this.
I found a solution and now I'm definitely going to hit additional backbonejs tutorials. Within the code, I was able to add the selector like so:
// Append container and title
var $el = this.$el.html(this.template({})).find('.mngmnt-main-sctn')
.
I'm always perplexed by stuff like this. You can't find any answers to solve the problem, then you try a 1,000 different things . . . and then the solution seems so simple and I always feel a bit foolish after such an experience.

Backbone infinite loop when creating models

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

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