In using Backbone.js, I've noticed that both views and models are given cids. I understand that if these classes are part of a collection, I can pull any of them by collection.getByCid. Is it at all possible to pull any class, outside of a collection, given its cid, using Backbone?
For example, if I have MyObject.Views.Tree = Backbone.View.extend({ });, I can create a new Tree view from var tree = new MyObject.Views.Tree();. Calling tree.cid returns a specific cid--something like view231. Is there any way to reference my tree view given only its cid? A global Backbone.getByCid method, perhaps?
ExtJS spoiled me and I felt the need to recreate something similar for Backbone. Maybe this will help you out too? I haven't tested it too much, but it's a very simple change. Just be careful of creating lots of things and not removing them, or you'll have a bunch of registered objects eating up memory.
Backbone.View.Registry = {
items: {},
register: function (id, object) {
this.items[id] = object;
},
lookup: function (id) {
return this.items[id] || null;
},
remove: function (id) {
delete this.items[id];
}
}
Backbone.RegisteredView = Backbone.View.extend({
initialize: function () {
Backbone.View.prototype.initialize.apply(this);
this.cid = this.options.id || this.cid; //just in case you want to assign a unique ID and lookup from that.
Backbone.View.Registry.register(this.cid, this);
},
remove: function () {
Backbone.View.prototype.remove.apply(this);
Backbone.View.Registry.remove(this.cid);
return this;
}
});
test = Backbone.RegisteredView.extend({
intialize: function () {
return $("<div></div>"); //Just return something for this example
}
});
div1 = new test({id: 'header_pane'});
div2 = new test();
console.log(Backbone.View.Registry.items); //Will have the header_pane and a cid based obj in it
ref = Backbone.View.Registry.lookup('header_pane');
console.log(ref); //Will be the header_pane object
div1.remove();
console.log(Backbone.View.Registry.items); //Will only have the cid based object in it
No.
I think you have a slight misunderstanding of the backbone programming model, as well as JavaScript in general. Backbone doesn't keep track of what you create; it only helps you create objects with specific prototypes (Models, Collections, etc.). It doesn't care at all what you do with them. The CID is just a convenience method you can use for indexing and cross-referencing, but you have to write the indices and cross-references yourself.
So if you create an object and don't keep a reference to it somewhere (in a collection, in your router, in another object), it becomes inaccessible and the JavaScript VM will eventually garbage collect it.
Related
I am using same el for more than 1 view like below. I'm not facing any problem till now. Is this good approach or should i do any changes?
<div id="app">
<div id="app-header"></div>
<div id="app-container"></div>
<div id="app-footer">
</div>
App View:
{
el: "#app",
v1: new View1(),
v2: new View2(),
render: function () {
if (cond1) {
this.v1.render();
} else if (cond2) {
this.v2.render();
}}
}
View 1:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
View 2:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
By reading your question, I do not really see what advantages you could possibly have using this approach rather than having the different div elements being the root el for your views 1, 2, 3 and using
this.$el.html(template)
in the render method.
Your approach could work for a small application, but I think it will become really hard to maintain as the application grows.
EDIT
I still do not really get your point, you could only initialize everything only once in both cases.
Here is a working Fiddle.
By the way I am changing the content by listening to the click event but this is to simplify the example. It should be done by the router.
I do use a mixin to handle such situation, I call it stated view. For a view with all other options I will send a parameter called 'state', render will in-turn call renderState first time and there after every time I set a 'state' renderState will update the view state. Here is my mixin code looks like.
var setupStateEvents = function (context) {
var stateConfigs = context.getOption('states');
if (!stateConfigs) {
return;
}
var state;
var statedView;
var cleanUpState = function () {
if (statedView) {
statedView.remove();
}
};
var renderState = function (StateView) {
statedView = util.createView({
View: StateView,
model: context.model,
parentEl: context.$('.state-view'),
parentView:context
});
};
context.setState = function (toState) {
if (typeof toState === 'string') {
if (state === toState) {
return;
}
state = toState;
var StateView = stateConfigs[toState];
if (StateView) {
cleanUpState();
renderState(StateView);
} else {
throw new Error('Invalid State');
}
} else {
throw new Error('state should be a string');
}
};
context.getState = function () {
return state;
};
context.removeReferences(function(){
stateConfigs = null;
state=null;
statedView=null;
context=null;
})
};
full code can be seen here
https://github.com/ravihamsa/baseapp/blob/master/js/base/view.js
hope this helps
Backbone Rule:
When you create an instance of a view, it'll bind all events to el if
it was assigned, else view creates and assigns an empty div as el for that view and bind
all events to that view.
In my case, if i assign #app-container to view 1 and view 2 as el and when i initialize both views like below in App View, all events bind to the same container (i.e #app-container)
this.v1 = new App.View1();
this.v2 = new App.View2();
Will it lead to any memory leaks / Zombies?
No way. No way. Because ultimately you are having only one instance for each view. So this won't cause any memory leaks.
Where does it become problematic?
When your app grows, it is very common to use same id for a tag in both views. For example, you may have button with an id btn-save in both view's template. So when you bind btn-save in both views and when you click button in any one the view, it will trigger both views save method.
See this jsFiddle. This'll explain this case.
Can i use same el for both view?
It is up to you. If you avoid binding events based on same id or class name in both views, you won't have any problem. But you can avoid using same id but it's so complex to avoid same class names in both views.
So for me, it looks #Daniel Perez answer is more promising. So i'm going to use his approach.
Usually I find my self needing to write an object with a specific functionality that it is a set of models.
Finally I extend a collection and add more functions that works with its model.
I think is better show you an example:
My app has a set of permissions, related with the user and/or the version of the platform.
var Permissions = Backbone.Collection.extend({
model: Permission,
hasAccess: function (moduleCode) {
....
},
allowAccess: function (moduleCode) {
....
},
...
With that methods I change the format of a property of a permission (the model). (My permissions are a concatenation of code with an string that identify the type of permission.)
A workmate tells me that it is wrong. In .NET he creates a class and he adds a private list and makes the changes to it. He does not inherit the list and changes it.
He would make a model and inside it he would add a collection property
this.set("permissionsCollection", new Backbone.Collection.extend({model: Permission}))
[Comment: I don't understand why he creates properties of everything, I think in his case it is not needed.] -> But this is another question
I think in a different way. I know the Collection has internally that list. I have a great potencial in Backbone.Collections, why do I have to use a model that it is not necessary? If I don't need that encapsulation... I think that it is not necessary, he is overprogramming in my opinnion.
Am I wrong? Did I not know how to use BackboneJS Collections?
Thank you in advance.
At the beginning I had something called helper with similar methods:
findAttr: function (model, name) {
var attrs = model.get('app_attrs');
if (attrs !== undefined) {
return this.findByAttrName(attrs, name);
}
},
findByAttrName: function (array, name) {
return _.find(array, function(a) {
if (a.attrName === name) {
return a;
}
});
}
The view code was more awkward:
render: function () {
var attr = helper.findAttr(this.model, 'user');
...
return this;
}
The only logical solution was to move these methods into the model (this.model in the above case). After refactoring I've got:
render: function () {
var attr = this.model.findAttr('user');
...
return this;
}
which is of course more readable than the previous solution.
I have a complex model served from my back end, which has a bunch of regular attributes, some nested models, and a couple of collections.
My page has two tables, one for invalid items, and one for valid items. The items in question are from one of the nested collections. Let's call it baseModel.documentCollection, implementing DocumentsCollection.
I don't want any filtration code in my Marionette.CompositeViews, so what I've done is the following (note, duplicated for the 'valid' case):
var invalidDocsCollection = new DocumentsCollection(
baseModel.documentCollection.filter(function(item) {
return !item.isValidItem();
})
);
var invalidTableView = new BookIn.PendingBookInRequestItemsCollectionView({
collection: app.collections.invalidDocsCollection
});
layout.invalidDocsRegion.show(invalidTableView);
This is fine for actually populating two tables independently, from one base collection. But I'm not getting the whole event pipeline down to the base collection, obviously. This means when a document's validity is changed, there's no neat way of it shifting to the other collection, therefore the other view.
What I'm after is a nice way of having a base collection that I can have filter collections sit on top of. Any suggestions?
I fleshed out my previous attempt and have come up with an extension to Backbone.Collection that does what I need.
collections.FilteredCollection = Backbone.Collection.extend({
initialize: function(items, options) {
if (_.isUndefined(options.baseCollection))
throw "No base collection to watch";
if (!_.isFunction(options.filterFunc)) {
throw "No filter to apply";
}
_.extend(this, options);
this.listenTo(this.baseCollection, 'all', this.reraise);
},
reraise: function (event) {
this.reset(this.baseCollection.filter(this.filterFunc), { silent: true });
var args = [].slice.call(arguments, 1);
this.trigger(event, args);
}
});
The one small issue I have with this is that I have to manually apply filterFunc to the baseCollection, then pass that in as the items parameter when instantiating a FilteredCollection, but that's something I can live with.
The below code is what I'm using to instantiate. Note that there's another almost-exact copy which is for the collection of ONLY VALID items, but any filters can be applied.
var allDocs = theModel.get('Documents');
var invalidOptions = {
baseCollection: allDocs,
filterFunc: function(item) {
return !item.isValidItem();
}
};
var invalidDocs = allDocs.filter(invalidOptions.filterFunc);
var invalidDocsCollection = new collections.FilteredCollection(
invalidDocs, invalidOptions
);
Here is my situation. I have a bunch of "Question" model inside a "Questions" collection.
The Question Collection is represented by a SurveyBuilder view.
The Question Model is represented by a QuestionBuilder view.
So basically you have an UL of QuestionBuilder views. The UL has a jQuery sortable attached (so you can reorder the questions). The question is once I'm done reordering I want to update the changed "question_number"s in the models to reflect their position.
The Questions collection has a comparator of 'question_number' so collection should be sorted. Now I just need a way to make their .index() in the UL reflect their question_number. Any ideas?
Another problem is DELETEing a question, I need to update all the question numbers. Right now I handle it using:
var deleted_number = question.get('question_number');
var counter = deleted_number;
var questions = this.each(function(question) {
if (question.get('question_number') > deleted_number) {
question.set('question_number', question.get('question_number') - 1);
}
});
if (this.last()) {
this.questionCounter = this.last().get('question_number') + 1;
} else {
this.questionCounter = 1;
}
But it seems there's got to be a much more straighforward way to do it.
Ideally whenever a remove is called on the collection or the sortstop is called on the UL in the view, it would get the .index() of each QuestionuBuilder view, update it's models's question_number to the .index() + 1, and save().
My Models,Views, and Collections: https://github.com/nycitt/node-survey-builder/tree/master/app/js/survey-builder
Screenshot: https://docs.google.com/file/d/0B5xZcIdpJm0NczNRclhGeHJZQkE/edit
More than one way to do this but I would use Backbone Events. Emit an event either when the user clicks something like done sorting, hasn't sorted in N seconds, or as each sort occurs using a jQuery sortable event such as sort. Listen for the event inside v.SurveyBuilder.
Then do something like this. Not tested obviously but should get you there relatively easily. Update, this should handle your deletions as well becuase it doesn't care what things used to be, only what they are now. Handle the delete then trigger this event. Update 2, first examples weren't good; so much for coding in my head. You'll have to modify your views to insert the model's cid in a data-cid attribute on the li. Then you can update the correct model using your collection's .get method. I see you've found an answer of your own, as I said there are multiple approaches.
v.SurveyBuilder = v.Page.extend({
template: JST["app/templates/pages/survey-builder.hb"],
initialize: function() {
this.eventHub = EventHub;
this.questions = new c.Questions();
this.questions.on('add', this.addQuestion, this);
this.eventHub.on('questions:doneSorting', this.updateIndexes)
},
updateIndexes: function(e) {
var that = this;
this.$('li').each(function(index) {
var cid = $(this).attr('data-cid');
that.questions.get(cid).set('question_number', index);
});
}
I figured out a way to do it!!!
Make an array of child views under the parent view (in my example this.qbViews maintains an array of QuestionBuilder views) for the SurveyBuilder view
For your collection (in my case this.questions), set the remove event using on to updateIndexes. That means it will run updateIndexes every time something is removed from this.questions
In your events object in the parent view, add a sortstop event for your sortable object (in my case startstop .question-builders, which is the UL holding the questionBuilder views) to also point to updateIndexes
In updateIndexes do the following:
updateIndexes: function(){
//Go through each of our Views and set the underlying model's question_number to
//whatever the index is in the list + 1
_.each(this.qbViews, function(qbView){
var index = qbView.$el.index();
//Only actually `set`s if it changed
qbView.model.set('question_number', index + 1);
});
},
And there is my full code for SurveyBuilder view:
v.SurveyBuilder = v.Page.extend({
template: JST["app/templates/pages/survey-builder.hb"],
initialize: function() {
this.qbViews = []; //will hold all of our QuestionBuilder views
this.questions = new c.Questions(); //will hold the Questions collection
this.questions.on('add', this.addQuestion, this);
this.questions.on('remove', this.updateIndexes, this); //We need to update Question Numbers
},
bindSortable: function() {
$('.question-builders').sortable({
items: '>li',
handle: '.move-question',
placeholder: 'placeholder span11'
});
},
addQuestion: function(question) {
var view = new v.QuestionBuilder({
model: question
});
//Push it onto the Views array
this.qbViews.push(view);
$('.question-builders').append(view.render().el);
this.bindSortable();
},
updateIndexes: function(){
//Go through each of our Views and set the underlying model's question_number to
//whatever the index is in the list + 1
_.each(this.qbViews, function(qbView){
var index = qbView.$el.index();
//Only actually `set`s if it changed
qbView.model.set('question_number', index + 1);
});
},
events: {
'click .add-question': function() {
this.questions.add({});
},
//need to update question numbers when we resort
'sortstop .question-builders': 'updateIndexes'
}
});
And here is the permalink to my Views file for the full code:
https://github.com/nycitt/node-survey-builder/blob/1bee2f0b8a04006aac10d7ecdf6cb19b29de8c12/app/js/survey-builder/views.js
I can successfully do this:
App.SomeCollection = Backbone.Collection.extend({
comparator: function( collection ){
return( collection.get( 'lastName' ) );
}
});
Which is nice if I want to have a collection that is only sorted by 'lastName'. But I need to have this sorting done dynamically. Sometimes, I'll need to sort by, say, 'firstName' instead.
My utter failures include:
I tried passing an extra variable specifying the variable to sort() on. That did not work. I also tried sortBy(), which did not work either. I tried passing my own function to sort(), but this did not work either. Passing a user-defined function to sortBy() only to have the result not have an each method, defeating the point of having a newly sorted backbone collection.
Can someone provide a practical example of sorting by a variable that is not hard coded into the comparator function? Or any hack you have that works? If not, a working sortBy() call?
Interesting question. I would try a variant on the strategy pattern here. You could create a hash of sorting functions, then set comparator based on the selected member of the hash:
App.SomeCollection = Backbone.Collection.extend({
comparator: strategies[selectedStrategy],
strategies: {
firstName: function () { /* first name sorting implementation here */ },
lastName: function () { /* last name sorting implementation here */ },
},
selectedStrategy: "firstName"
});
Then you could change your sorting strategy on the fly by updating the value of the selectedStrategy property.
EDIT: I realized after I went to bed :) that this wouldn't quite work as I wrote it above, because we're passing an object literal to Collection.extend. The comparator property will be evaluated once, when the object is created, so it won't change on the fly unless forced to do so. There is probably a cleaner way to do this, but this demonstrates switching the comparator functions on the fly:
var SomeCollection = Backbone.Collection.extend({
comparator: function (property) {
return selectedStrategy.apply(myModel.get(property));
},
strategies: {
firstName: function (person) { return person.get("firstName"); },
lastName: function (person) { return person.get("lastName"); },
},
changeSort: function (sortProperty) {
this.comparator = this.strategies[sortProperty];
},
initialize: function () {
this.changeSort("lastName");
console.log(this.comparator);
this.changeSort("firstName");
console.log(this.comparator);
}
});
var myCollection = new SomeCollection;
Here's a jsFiddle that demonstrates this.
The root of all of your problems, I think, is that properties on JavaScript object literals are evaluated immediately when the object is created, so you have to overwrite the property if you want to change it. If you try to write some kind of switching into the property itself it'll get set to an initial value and stay there.
Here's a good blog post that discusses this in a slightly different context.
Change to comparator function by assigning a new function to it and call sort.
// Following example above do in the view:
// Assign new comparator
this.collection.comparator = function( model ) {
return model.get( 'lastname' );
}
// Resort collection
this.collection.sort();
// Sort differently
this.collection.comparator = function( model ) {
return model.get( 'age' );
}
this.collection.sort();
So, this was my solution that actually worked.
App.Collection = Backbone.Collection.extend({
model:App.Model,
initialize: function(){
this.sortVar = 'firstName';
},
comparator: function( collection ){
var that = this;
return( collection.get( that.sortVar ) );
}
});
Then in the view, I have to M-I-C-K-E-Y M-O-U-S-E it like this:
this.collections.sortVar = 'lastVar'
this.collections.sort( this.comparator ).each( function(){
// All the stuff I want to do with the sorted collection...
});
Since Josh Earl was the only one to even attempt a solution and he did lead me in the right direction, I accept his answer. Thanks Josh :)
This is an old question but I recently had a similar need (sort a collection based on criteria to be supplied by a user click event) and thought I'd share my solution for others tackling this issue. Requires no hardcoded model.get('attribute').
I basically used Dave Newton's approach to extending native JavaScript arrays, and tailored it to Backbone:
MyCollection = Backbone.Collection.extend({
// Custom sorting function.
sortCollection : function(criteria) {
// Set your comparator function, pass the criteria.
this.comparator = this.criteriaComparator(criteria);
this.sort();
},
criteriaComparator : function(criteria, overloadParam) {
return function(a, b) {
var aSortVal = a.get(criteria);
var bSortVal = b.get(criteria);
// Whatever your sorting criteria.
if (aSortVal < bSortVal) {
return -1;
}
if (aSortVal > bSortVal) {
return 1;
}
else {
return 0;
}
};
}
});
Note the "overloadParam". Per the documentation, Backbone uses Underscore's "sortBy" if your comparator function has a single param, and a native JS-style sort if it has two params. We need the latter, hence the "overloadParam".
Looking at the source code, it seems there's a simple way to do it, setting comparator to string instead of function. This works, given Backbone.Collection mycollection:
mycollection.comparator = key;
mycollection.sort();
This is what I ended up doing for the app I'm currently working on. In my collection I have:
comparator: function(model) {
var methodName = applicationStateModel.get("comparatorMethod"),
method = this[methodName];
if (typeof(method === "function")) {
return method.call(null, model);
}
}
Now I can add few different methods to my collection: fooSort(), barSort(), and bazSort().
I want fooSort to be the default so I set that in my state model like so:
var ApplicationState = Backbone.Model.extend({
defaults: {
comparatorMethod: "fooSort"
}
});
Now all I have to do is write a function in my view that updates the value of "comparatorMethod" depending upon what the user clicks. I set the collection to listen to those changes and do sort(), and I set the view to listen for sort events and do render().
BAZINGA!!!!