Backbone view's model is missing, but I don't understand why - backbone.js

I'm having a simple but confusing problem. I have the following piece of code:
<div id="restaurant_locations"></div>
<script type="text/javascript">
$(function() {
window.router = new Lunchhub.Routers.RestaurantLocationsRouter({
restaurantLocations: <%= #restaurant_locations.to_json.html_safe -%>
});
Backbone.history.start({pushState: true});
});
</script>
which throws this error:
Uncaught TypeError: Cannot call method 'toJSON' of undefined
If I take out the {pushState: true} part, though, and just do Backbone.history.start() with no arguments, it works just fine.
Next to the error, it says show_view.js: 19. Here's what that part of show_view.js looks like:
ShowView.prototype.template = JST["backbone/templates/restaurant_locations/show"];
ShowView.prototype.render = function() {
$(this.el).html(this.template(this.model.toJSON())); // LINE 19
Uncaught TypeError: Cannot call method 'toJSON' of undefined
return this;
}
So I guess this.model is undefined. Here's the show_view CoffeeScript:
Lunchhub.Views.RestaurantLocations ||= {}
class Lunchhub.Views.RestaurantLocations.ShowView extends Backbone.View
template: JST["backbone/templates/restaurant_locations/show"]
render: ->
$(#el).html(#template(#model.toJSON() ))
return this
If I can make #model be what it needs to be, I guess it might fix the problem. But I don't know where #model comes from or anything.
What do I need to do?
Edit: I got a little further. In the show function below, id is set to "restaurant_locations", and there's of course no member of #restaurantLocations with an id of restuarant_locations. The fact that id set set to restaurant_locations makes a certain amount of sense; the URL I'm hitting is http://localhost:3000/restaurant_locations. But it seems like it should be calling the index function, not show, if that's the URL to which I'm going.
class Lunchhub.Routers.RestaurantLocationsRouter extends Backbone.Router
initialize: (options) ->
#restaurantLocations = new Lunchhub.Collections.RestaurantLocationsCollection()
#restaurantLocations.reset options.restaurantLocations
routes:
"new" : "newRestaurantLocation"
"index" : "index"
":id/edit" : "edit"
":id" : "show"
".*" : "index"
newRestaurantLocation: ->
#view = new Lunchhub.Views.RestaurantLocations.NewView(collection: #restaurantLocations)
$("#restaurant_locations").html(#view.render().el)
index: ->
#view = new Lunchhub.Views.RestaurantLocations.IndexView(restaurantLocations: #restaurantLocations)
$("#restaurant_locations").html(#view.render().el)
show: (id) ->
restaurant_location = #restaurantLocations.get(id)
#view = new Lunchhub.Views.RestaurantLocations.ShowView(model: restaurant_location)
$("#restaurant_locations").html(#view.render().el)
edit: (id) ->
restaurant_location = #restaurantLocations.get(id)
#view = new Lunchhub.Views.RestaurantLocations.EditView(model: restaurant_location)
$("#restaurant_locations").html(#view.render().el)

id is set to "restaurant_locations", and there's of course no member of #restaurantLocations with an id of "restuarant_locations".
Sounds like you have a routing problem so let us look at your routes:
routes:
"new" : "newRestaurantLocation"
"index" : "index"
":id/edit" : "edit"
":id" : "show"
".*" : "index"
I see several problems there. First of all, routes are not regexes so ".*" doesn't match what /.*/ does (i.e. any sequence of characters), it actually matches any number of periods (i.e. /^.*$/); you can run it through _routeToRegex yourself to see what happens to '.*':
var namedParam = /:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
//...
_routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&')
.replace(namedParam, '([^\/]+)')
.replace(splatParam, '(.*?)');
return new RegExp('^' + route + '$');
},
The next problem is that ":id" matches almost anything. routeToRegexp converts ":id" to /^([^/]+)$/; that regex matches any non-empty sequence of non-slashes and in particular, that will match what most of your other routes match.
So when you expect to hit '.*': 'index' from /restaurant_locations, you're actually hitting ':id': 'show' and you're getting lucky that '/new' and '/index' aren't also getting matched by ':id'. Keep in mind that the order of elements in a (Java|Coffee)Script object is implementation defined so the order that they appear in in your source code really doesn't matter; many JavaScript implementations will respect the source order but never depend on it.
Since the route order doesn't matter, you have to look at your routes keys as a simple unordered set and you--the human--must ensure that your route patterns really do match distinct things. If you do need overlapping route patterns and you need them to get matched in a specific order, then you can use the route method in your router's initialize to manually add the routes (as routing patterns or regexes) in the required order.
Executive Summary: Fix your routes so that they match distinct things.

Related

Backbone does POST instead of PUT on updates when composite key is used

I'm using a composite key in my model and generate the ID based on my composite key:
app.Assignment = Backbone.Model.extend({
idAttribute : [ 'personId', 'jobId' ],
parse : function(resp) {
resp.id = resp.personId + "_" + resp.jobId;
return resp;
}
});
but Backbone still thinks that all instances of Assignment are new, allthough I'm setting the id in the parse method when fetching them from the API. As a result Backbone does no DELETEs and does a POST instead of PUT on updates. How can I work around this or what is the "right way" to do it?
Update:
Looks like replacing resp.id with this.id solves the issue.
The results of the parse method of a Backbone.Model are passed to the set method, which sets the attributes of the model. The point of confusion for you I think is that the model's ID isn't one of it's attributes; it's one of its properties.
So, what happens is this:
Your raw data comes back from the server and is passed to parse
That same raw data, now augmented with a id attribute, is passed to set
set looks and your idAttribute ([ 'personId', 'jobId' ]) and all of the keys in the raw data
Since none of those keys match the idAttribute, none of them are used as the model's ID, and thus you get your problem.
Your solution of setting this.id inside parse works, but it might cause problems down the road because parse is generally designed to operate on it's input (the raw data), not to modify the model itself; that part is supposed to happen next when set is called. A cleaner solution would instead be to do something like the following:
app.Assignment = Backbone.Model.extend({
// note that no idAttribute is specified, leaving it as the default "id"
parse : function(resp) {
resp.id = resp.personId + "_" + resp.jobId;
return resp;
}
}
Or, if you want a different ID attribute ...
app.Assignment = Backbone.Model.extend({
idAttribute: 'personAndJobId',
parse : function(resp) {
resp.personAndJobId = resp.personId + "_" + resp.jobId;
return resp;
}
}
Aside from the idAttribute issues here, you can always force Backbone to use a certain HTTP method via the type options passed to save().
model.save(null, { type: 'put' })
I've never work with composite ID in Backbone, but I think this could be an easy answer to your problem:
initialize: function() {
this.set("id", this.generateID());
},
generateID = function () {
return this.personId + + "_" + this.jobId;
}
With this code in you Backbone model definition you are creating a unique ID for each model and you shouldn't have problems for update and save it (and you don't need to set any idAttribute).

Underscore, can't call replace of undefined

I've experienced this error before and tried the solutions I've found on SO for it, but I can't get around it in this case by trying the solutions I've found. I have a question_template that I placed in the header of my index file with the js script tags at the bottom of the file. In the initializer to the view, I get the template using jQuery html function, and the console log shows the template is retrieved from the index.html. However, when i try to insert it into underscore _.template, it's triggering the can't call replace of undefined error
var QuestionView = Backbone.View.extend({
el: $(".east"),
initialize: function(){
var template = $('#question_template').html();
console.log(template);
this.template = _.template(template); #error triggered
},
Since I'm able to log the template, I don't see what my problem is? This is part of the underscore code.
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
First question: What would 'text' represent, which, in my case, is undefined? I would have thought that 'text' is the template, but since I can log my template how is it undefined?
I also have all of the js code (including the initialization of the Question view) wrapped in the document ready, which in other SO questions on this issue was the solution
$(function() {
...code ommitted...
var question_view = new QuestionView({ model: game});
});
Second question: is there anything else I can try
Update
Note, I subsequently pass the model data to the template but it never gets that far because the error is triggered
**$(this.el).html(this.template(response));**
I prepare the templates in three steps
1. var template = $('#question_template').html();
console.log(template);
2. this.template = _.template(template);
3. $(this.el).html(this.template(response));
You should pass the model data to the template,
this.template = _.template(template, this.model.toJSON());

How to avoid using Backbone-relational, a simple blog backbone app with comments relating to posts

Backbone relational is too messy for me and I can't seem to debug it. I am trying to avoid using this asset.
I have two collections in my Backbone app.
Voice.Collections.Posts and Voice.Collections.Comments
This is my router:
class Voice.Routers.Posts extends Backbone.Router
routes:
'': 'index'
initialize: ->
#collection = new Voice.Collections.Posts()
#collection.reset($('#container').data('posts').reverse())
index: ->
view = new Voice.Views.PostsIndex(collection: #collection)
$('#container').html(view.render().el)
I want my router to have a method that filters my comment collection according to a url with the post id ( as my comments - post relational key, post_id) so basically "posts/12"(posts/:id) will call a function showComments: (id) -> which will take the id and initialize a collection of comments which only contain comments where 'post_id' is equal to 12 ("id").
Could I sort the collection from my router?
something like this? (this doesnt work)
class Voice.Routers.Posts extends Backbone.Router
routes:
'': 'index'
'post/:id/': 'showComments'
initialize: ->
#collection = new Voice.Collections.Posts()
#collection.reset($('#container').data('posts').reverse())
index: ->
view = new Voice.Views.PostsIndex(collection: #collection)
$('#container').html(view.render().el)
showComments: (id) ->
#comCollection = new Voice.Views.Collections.Comments()
#comCollection = #comCollection.where ->
#model.get('post_id') = id
comview = new Voice.Views.CommentsIndex(collection: #comCollection)
$('#comContainer').html(comview.render().el)
but this doesn't work because the #comCollection needs to be intialized. I'm just not sure how I should do this. I would also settle for the comment collection being rendered as view from another views event trigger.Help is appreciated.
EDIT:
Would I have to use Backbone.navigate? Backbone.navigate creates a bad smell.
My CoffeeScript is a bit rusty, so I can't remember exactly what:
#comCollection = #comCollection.where ->
#model.get('post_id') = id
translates as in normal Javascript. However, it absolutely should work if used right, so perhaps if you tried a simpler syntax:
this.comCollection = this.comCollection.where({post_id: id});
you might have better success? If not, you may want to drop in to the debugger and check what #comCollection actually has in it after you make that call.

Uncaught Error: A "url" property or function must be specified for a CollectionView

I know this error has come up a few times, but I'm still not sure how to make this work appropriately..
My magic begins here :
var list_edit_member_view = new app.views.ListMemberEdit({
el: $("#enterprise_member_list_edit_container"),
list_ids: list_ids
});
list_edit_member_view.render();
And this loads this View (ListMemberEdit.js) which has this in the render() :
this.list_edit_member_view = new app.views.CollectionView({
el: $("#enterprise_member_list_edit_container"),
collection: app.peers,
list_item: app.views.ListMemberEditSelection,
list_item_options: {list_ids: this.options.list_ids}
});
Which loads a CollectionView view that renders its list_item_options as model views.. It is within this file (ListMemberEditSelection.js), that when I perform this.destroy, it will return :
Uncaught Error: A "url" property or function must be specified
So this makes me think that the Model or the Model URL is not being defined.. I'm just not sure where to put this since it works very similar to my other partials that are doing roughly the same thing..
Any thoughts? My apologies for the vagueness. Let me know if there's anything else you would like to look at!
I'm curious if its possible to see where this URL attribute would be written within the Object Model or Collection itself.
This is because destroy() function will call Backbone.sync to update the server too, not only your models in the frontend. http://backbonejs.org/#Model-destroy
So, if you're using REST to sync your data, you'll need to set a url property in your model so Backbone know where to send request:
Backbone.Model.extend({
url: "http://myapi.com/"
})
To allow more flexibility, you can also set a urlRoot: http://backbonejs.org/#Model-urlRoot
I had a similar problem, I removed the "id":"" from my models default values and the problem was solved.
I did receive similar error
Try this: I am just making an assumption what your model might look like
window.MyModel = Backbone.Model.extend({
url: function(){
return this.instanceUrl;
},
initialize: function(props){
this.instanceUrl = props.url;
}
}
Please look at this question that I had posted myself for more details: https://stackoverflow.com/a/11700275/405117
I am providing this reference as the answers here helped me better understand
Hope this helps!

backbone routes - ordering and filtering

In my Backbone app, on my collection I have numerous sorting methods, when rendering the views based on the collection I am currently using a global var set via the route (I do it with a global as other actions add to the collection and I want the last ordering to be used). For example
routes : {
"" : "index",
'/ordering/:order' : 'ordering'
},
ordering : function(theorder) {
ordering = theorder;
listView.render();
},
then in my view
if (typeof ordering === 'undefined') {
d = this.collection.ordered();
}
else if(ordering == 'owners') {
d = this.collection.owners();
}
_.each(d, function(model){
model.set({request : self.model.toJSON()});
var view = new TB_BB.OfferItemView({model : model});
els.push(view.render().el);
});
Where ordered and owners are the 2 ordering methods.
So my first question is, based on routes could someone advice a better way of implementing above? This view gets rendered in multiple places hence me using a global rather than passing a ordered var to the method?
Second question is - I would like to also add some filtering, so lets say I want to sort by 'price' but also do some filtering (lets say by multiple categories id). How could I add a flexible 'route' to deal with filtering.
I guess I could do
routes : {
"" : "index",
'/ordering/:order/:filter1/:filter2' : 'ordering'
},
So the filter1 and filter2 would be the subsequent filtering, but if the filters could be 0 or 100 this will not work. Could anyone offer a solution?
Well, first you should be using Backbone's built-in ability to auto-sort collections. You can take advantage of this by defining a comparator function on your collection. This gives you all kinds of wins right out of the box — for example, the collection will re-sort itself every time you add or remove something from it, based on your comparator. If you want to define multiple sort functions, just define them all as functions and then update comparator when you need to. Then you can ditch that ugly global var.
For your second question, I'm not totally sure what you mean by "if the filters could be 0 or 100 this will not work." If you mean that you'll run into trouble if you don't specifiy all of the filters, then that's true. But you can use a wildcard to fix that. Here's what that might look like:
// your routes look like this:
routes : {
'/ordering/:order/filters/*filters' : 'ordering' // your routes will look like: /ordering/price/filters/filter_one/filter_two/filter_three
},
ordering: function (order, filters) {
filters = filters.split('/'); // creates an array of filters: ['filter_one', 'filter_two', 'filter_three']
listView.render(filters); // pass your filters to the view
}
// listView.render() looks like this:
render: function(filters) {
collection = this.collection;
_.each(filters, function (filter) {
collection = collection.filter(function () {
// your actual filtering code based on what the filter is
});
});
}

Resources