Disclaimer: I'm new to Backbone.js (coming from AngularJS), so I may have an inaccurate mental model of how this is supposed to work.
I have an object, characterNodes, which I'm making an attribute on my model. characterNodes looks something like this:
var characterNodes = {
character_1: {
stories: [// list of Stories]
},
character_2: {
stories: [// list of Stories]
}
...
}
My Backbone Model looks something like this:
var StoryGraph = joint.dia.Graph.extend({
initialize: function() {
// Call parent constructor
StoryGraph.__super__.initialize.apply(this, []);
this.set('characterNodes', characterNodes);
this.on('change:characterNodes', function() {
alert('test');
});
}
});
Each Story has a property "isUnlocked" which is changed elsewhere in the application. I want to fire an event (ie. that is, the alert 'test' should pop up) whenever this property is changed. With the code as it is above, the event never seems to fire.
I can't get a clear understanding from the Backbone docs whether this is supposed to work - does on('change:characterNodes') fire whenever any property (or sub-property, or sub-sub-property, etc) of characterNodes changes? Or only when the pointer to the object changes, that is, when it's replaced with another object? Or am I doing something else wrong? Thanks!
Backbone doesn't do any magic, basically, the change event is fired only if you set the "characterNodes" to a new object. If you're changing a nested property of that object, Backbone doesn't know it happened. You have three options: a) Change the whole object (e.g. by creating a copy), b) fire the change event manually (m.trigger("change:characterNodes")) whenever you change a nested property, c) Do not use nested objects for this. Have "character1_Stories" as a top level property.
Options c) is preferable. Try to keep properties in your models flat. Option a) is also fine but it has the disadvantage of having to copy the object. Option b) is not recommended. This is because Backbone keeps track of the previous value of the model properties (m.previous("characterNodes")). If you change a nested property, the previous value will have the same reference to the same object as the new value, therefore, it won't reflect its previous state.
Try to call a function instead define the function, and pass the third argument to keep the context call.
Something like this:
this.on('change:characterNodes', this.AlertSomething, this);
Hope it helps.
Related
I have a Backbone view whose render method creates and caches some JavaScript objects then renders them out to SVG nodes in the DOM. So (very simplistically), I'm doing something like this:
render: function() {
// Create objects and cache them to the view...
this.someObjects = [obj1, obj2, ...];
// Render objects...
this.someObjects.forEach(function(obj) {
obj.render(); // Each object knows how to render itself...
})
return this;
}
My view events hash is returned from a function, and contains event handlers which close over certain variables which depend on the objects cached when rendering. So (again simplistically, just for illustration purposes), I'm doing something like this:
events: function() {
var getRenderedObjects = function() {
return this.someObjects;
};
var renderedObjects = getRenderedObjects();
return {
'click #someDiv .someClass' : function() {
console.log(renderedObjects);
}
}
}
The problem is that Backbone sets up the bindings for the events hash before the view initialization function runs, which is where I need to do an explicit render. So this.someObjects will be undefined. Before moving to Backbone all my event handlers were defined and bound after rendering the view, which seemed natural anyway. I have found a workaround, which is to set the events hash manually on initialization, after rendering, and then call delegateEvents() to ensure they're bound. This works, although I found that within the handlers this no longer refers to the view object, but rather to the global window object again. It all seems rather cumbersome and a bit hackish, however.
I guess I could leave renderedObjects as null and set it conditionally in the handler which requires it (so if it's not set, set it, else do nothing). That seems ugly, though. Is there a better way?
I'm trying to retrieve the model, because I need to access certain attributes, but I cannot. I'm inside a view, which extends another one. This is how the view looks like:
var ImageGridControlView = GridControlView.extend({
//.... stuff ....
alert(this.model.get('property')) //This gives me an error in console and nothing is alerted.
});
In the class that the above code extends, the model can be retrieved, like this:
var GridControlView = ControlView.extend({
//.... stuff ....
alert(this.model.get('property')) //This shows the property value correctly
});
Why can't the model be retrieved from ImageGridControlView? What is the persistence of models across different classes, children, etc? I'm new to Backbone, and the official documentation only covers models superficially.
Your snippets are too small to actually find the problem, so we have to just guess. Most likely candidates are:
you are accessing this.model inside a function triggered by an event binding that is not properly bound, so this isn't actually your view instance. It could be a the source DOM element of the window object instead.
You aren't passing a model property in the options object provided to the view's constructor
First off - I am a MarionetteJS noob.
I am having trouble making an ItemView display a loading message or throbber while it is being fetched. This is especially problematic when this ItemView is being displayed from a deep link in Backbone's history (i.e. the ItemView is the first page of the app being displayed since the user linked directly to it). I want to indicate that the page is loading (fetching), preferably with a simple view, and then show the real templated view with the fetched model.
I have seen other answers on SO like Marionette.async, (which has been deprecated) and changing the template during ItemView.initalize().
Anybody (Derrick?) got any suggestions or best practices here?
UPDATE:
I am getting the model from the collection using collection.get(id), not using model.fetch() directly.
After thinking about this, the real question is where should this be implemented:
I could change my controller to see if the model exists in the collection (and if the collection is loaded) and decide which view to show accordingly. this seems like a lot of boilerplate everywhere since this could happen with any ItemView and any controller.
I could change my ItemView initialize to test for existence of the model (and a loaded collection), but same comment here: every ItemView could have this problem.
UPDATE 2:
This is what I ended up with, in case anybody else want this solution:
app.ModelLayout = Backbone.Marionette.Layout.extend({
constructor: function(){
var args = Array.prototype.slice.apply(arguments);
Backbone.Marionette.Layout.prototype.constructor.apply(this, args);
// we want to know when the collection is loaded or changed significantly
this.listenTo(this.collection, "reset sync", this.resetHandler);
},
resetHandler: function () {
// whenever the collection is reset/sync'ed, we try to render the selected model
if (this.collection.length) {
// when there is no model, check to see if the collection is loaded and then try to get the
// specified id to render into this view
this.model = this.collection.get(this.id);
}
this.render();
},
getTemplate: function(){
// getTemplate will be called during render() processing.
// return a template based on state of collection, and state of model
if (this.model){
// normal case: we have a valid model, return the normal template
return this.template;
} else if (this.collection && this.collection.isSyncing) {
// collection is still syncing, tell the user that it is Loading
return this.loadingView;
} else {
// we're not syncing and we don't have a model, therefore, not found
return this.emptyView;
}
}
});
And here is how to use it:
// display a single model on a page
app.Access.Layout.CardLayout = app.ModelLayout.extend({
regions: {
detailsRegion:"#detailsRegion",
eventsRegion:"#eventsRegion"
},
template:"CardLayout", // this is the normal template with a loaded model
loadingView:"LoadingView", // this is a template to show while loading the collection
emptyView:"PageNotFoundView", // this is a template to show when the model is not found
onRender : function() {
this.detailsRegion.show( blah );
this.eventsRegion.show( blah );
}
});
thanks!
For the ItemView
I think you can add a spinner in your initialize function, I really like spin.js http://fgnass.github.io/spin.js/ because its pretty easy and simple to use, and you can hide the spinner in the onRender function of the Itemview
For The CollectionView
in the CollectionView you could handle it like this....
Take a look at the solution that Derick posted..
https://github.com/marionettejs/backbone.marionette/wiki/Displaying-A-%22loading-...%22-Message-For-A-Collection-Or-Composite-View
I'd suggest using jQuery deferreds:
Start fetching your data, and store the return value (which is a jQuery promise)
Instanciate your content view
Show your loading view
When the promise is done, show the view containing the content
I've talked about implementing this technique on my blog:
http://davidsulc.com/blog/2013/04/01/using-jquery-promises-to-render-backbone-views-after-fetching-data/
http://davidsulc.com/blog/2013/04/02/rendering-a-view-after-multiple-async-functions-return-using-promises/
The issue with the solution linked by Rayweb_on, is that your loading view will be displayed any time your collection is empty (i.e. not just when it's being fetched). Besides, the ItemView doesn't have an emptyView attribute, so it won't be applicable to your case anyway.
Update:
Based on your updated question, you should still be able to apply the concept by dynamically specifying which template to use: https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.view.md#change-which-template-is-rendered-for-a-view
Then, when/if the data has been fetched successfully, trigger a rerender in the view.
My backbone.js model has an array property. I bound the change event to save().
After sync() (triggered by save(), my app server returns an identical JSON, but backbone thinks the array has been changed (due to a different reference to the array I guess?), and trigger changes again. Then an infinite loop occurs.
save() -> sync() -> triggered `change` -> save()...
What shall I do?
Idea: I can bind the change event to a function that checks if the changed attributes are of type object/array, and do a deep comparison and call save only if the array/object really changed. If true then save()?
Thanks!
Try the Edge version of Backbone (master branch) this behavior changed after 0.9.9 - see https://github.com/documentcloud/backbone/pull/2004
Backbone has a special option on many methods to prevent just this sort of issue: silent:true. If you pass that option to your save method, the resulting sync won't trigger a change event.
So, if you want to set your change event handler to save silently, something like:
changeHandler: function() {
this.save({silent:true});
}
should do the trick.
I took a small code from backbone home site, and consoled the function, in this example, sidebar.on('change:color'), takes the function. but it requires two parameter, one is abiously we need that is 'color', and we defining the element inside the function, still why we giving another parameter as 'model' here, what that parameter does?
if i remove that parameter send only the color, the function doesn't work at all... any one help me to understand this?
sample function here:
var Sidebar = Backbone.Model.extend({
promptColor : function(){
var cssColor = prompt('Please enter a css color');
this.set({color:cssColor});
}
});
window.sidebar = new Sidebar;
sidebar.on('change:color',function(model,color){ // what model parameter do here?
console.log(model);
$('#sidebar').css({
background:color
})
})
sidebar.set({color:'green'});
sidebar.promptColor();
when i console the model i got this:
d
_callbacks: Object
_changing: false
_escapedAttributes: Object
_pending: Object
_previousAttributes: Object
_silent: Object
attributes: Object
changed: Object
cid: "c1"
__proto__: x
It is possible that you want to know which model was affected.
Consider a case where you are listening to an event on a collection instead. Which model's color value was modified? The model parameter tells you this.
Also, consider a case where the same handler is listening to "change:color" on multiple models. Again, you might want to know which model sent the event.
Just like in other event-driven environments, the "sender" is always passed along with the event data. model, in this case, is the sender.
Backbone is a Javascript MVC framework. (Unlike standard MVC, Backbone doesn't have controllers, instead it has collections).
The model you are receiving is a standard model from the MVC paradigm. Model's are the underlying data structures that hold the data that the user is working with.
When you do
sidebar.on('change:color', function(model, color) {
// some code here
});
you are attaching an event handler to the sidebar model. Specifically, you are saying that when the color attribute on this model changes, call the function. Since this event can and will trigger at a later point in time, Backbone passes the event handler function two arguments: the first is the model on which the event fired, and the second is the attribute that changed.
The arguments are passed in a specific order, that is model is the first argument, and the changed attribute is the second. Thus if you omit the model argument from your event handler function, the passed in model gets assigned to color, and color doesn't get assigned to any argument.
Recommended reading:
More about MVC and models
More about backbone models