I am at the end of my mental leash with this one...
I am attempting to render a view to the screen that contains a model object attribute. I am able to get the html to render but the model attribute is not inserted into the div as expected but is instead rendering as undefined.
The thing that makes this so frustrating is that when I log the view object to the console I am able to inspect it and see that the correct model is associated with it and that the user's attributes are indeed present via this > model > attributes. However, if try to access the attributes directly in my code and then log that to the console I get undefined.
router show action
show: function(customer_id){
var customer = new backbone_data.Models.Customer({id: customer_id});
customer.fetch();
var customerView = new backbone_data.Views.CustomerView({model: customer});
$("#app_container").append(customerView.render().el);
}
render function in view class -- both of these do not work
render: function(){
this.$el.html("<h3>Hello, my age is "+this.model.get('age')+"</h3>");
return this;
}
template: _.template("<h3>Hello, my age is <%= age %></h3>"),
render: function(){
this.$el.html(this.template(this.model.attributes));
return this;
}
console image showing object attributes
I am logging these from inside the view's render function like so:
render: function(){
console.log(this);
console.log(this.model.attributes.age);
this.$el.html("<h3>Hello, my age is "+this.model.get('age')+"</h3>");
return this;
}
You can see from the screenshot that while the age attribute is accessible when logging the view object in the console, it is undefined when logging it directly in my code and rendering as undefined in the view.
http://imgur.com/P453dKL
Thanks for the help!!!!!!!
customer.fetch() performs an asynchronous request to populate the model's data. Your code creates the view and passes it the model immediately after you fetch(). Try this:
customer.fetch({
success: function(data)
{
var customerView = new backbone_data.Views.CustomerView({model: data});
$("#app_container").append(customerView.render().el);
}
});
That should wait until the fetch() is completed and ensure that the data you want has been retrieved.
Related
Can someone tell me how to re fetch a Backbone collection after calling collection's create function when I create a new model?
When I call fetch on my collection after creating new model, sometimes I'm getting that model and sometimes not.
My problem is when I create a new model in my collection, I'm not getting the id back of my model and then I can't update it immediately, I need to refresh the page and then I got the id of the created model.
I tried with listenTo but I can't use it because I need to send more collections to one function.
And that my view for my bootstrap modal, on save I'm creating my model it persists to database and I'm getting all attributes in my console when I create it except models id.
Backbone view:
app.types.EditView = Backbone.View.extend({
tagName: "div",
$container: $('#containerEdit'),
template: _.template($('#itemEdit-template').html()),
events:
{
"click .save": "save",
},
initialize: function(options)
{
this.options = options;
this.$container.html(this.render());
this.start();
this.end();
},
render: function()
{
this.$el.html(this.template());
return this.$el;
},
save: function()
{
console.log("save");
$('#openModal').modal('hide');
var dan = this.model.dan_u_tjednu_usera.datum;
var mjesec = this.model.dan_u_tjednu_usera.mjesecBrojevi;
var godina = this.model.dan_u_tjednu_usera.godina;
var start = $("#start").val();
var end = $("#end").val();
var user_id = this.model.user.id;
this.model.shifts.create({day: dan, month: mjesec, year: godina, time_from: start, time_to: end, user_id: user_id});
this.options.model.el.html($("<td href='#openModal' width='25%' align='center' class='list-group test' scope='row'>" + start + " - " + end + " " + "Admin" + "</td>"));
this.model.shifts.fetch({sync: true});
console.log("test", this.model.shifts);
}
Here you can see that in my response im not getting the id attribute, on create.
And here you can see when i click on my cell i log my collection and i have not the id attribute of the created model here. And im not getting the id attribute it too when i log this.model
This is because the request sent to the server when you call Collection.create is asynchronous, the Javascript code will continue to execute before the server receives and responds to the request.
If you want to have the Model updated with the ID coming back from the server, you can specify {wait: true} in the Collection.create call. This will mean that the Collection will not have the Model added straight away, but instead only when the server responds (successfully).
In this case you should not run the fetch immediately afterwards, as it will also need to wait for the create operation to complete. You should setup any following actions to trigger when the create operation has completed. Here is an example:
var model = collection.create({field: 'abc'}, {wait: true});
model.once('sync', function() { window.alert(model.id); });
Backbone's create
Convenience to create a new instance of a model within a collection.
Equivalent to instantiating a model with a hash of attributes, saving
the model to the server, and adding the model to the set after being
successfully created.
There's no need to fetch a collection after a create, the model id and any other field are automatically merged within its attributes hash.
Fetch after model creation
While mikeapr4 is not wrong, his example could be improved.
The { wait: true } is unnecessary if the only problem comes from the fetch, not from the model already being inside the collection.
Also, once should be avoided as it's the "old" way, and instead listenToOnce should be used. See Difference between ListenTo and on.
If you really want to fetch once a model is created, using events is overkill here, and instead, using the success callback is best:
save: function() {
// ..snip...
this.model.shifts.create({ /* ...snip... */ }, {
context: this,
success: this.onModelCreated
});
},
onModelCreated: function() {
// the model is now created and its attributes are up-to-date
this.model.shifts.fetch();
}
Other notes on your code
There are no sync option in Backbone. Only a "sync" event and a sync function.
Avoid using the global jQuery selector (like $('.class-name')) and instead, whenever the element is within the view's element, use this.$('.class-name').
Also, cache the jQuery element to avoid the costly search of the find method.
Like $("#start") could be cache and reused. Only reset the cached elements when re-rendering.
The Backbone .render function should return this by convention.
Then, your rendering call should look like:
this.$container.html(this.render().el); // el is enough
I've set an attribute currentPage on my Backbone collection, however I seem to not be able to log it specifically.
onRender: function () {
App.log('this.collection.currentPage onRender')
App.log(this.collection)
App.log(this.collection.accountID)
App.log(this.collection.currentPage)
App.log(this.collection.get('currentPage'))
}
I do the fetch then show, and within onRender, the following console log is produced
where this.collection.currentPage and this.collection.get('currentPage') are undefined even though I can see currentPage as a defined ("2") attribute in the log output of this.collection. this.collection.accountID is working how I expected currentPage to work.
What am I doing wrong?
Edit: I am setting the attribute on the success of .fetch because the data is returned in the response headers. Something like this,
getAccountSegments: function (accountID) {
var accountSegmentsCollection = new Entities.Collections.AccountSegmentCollection({
accountID: accountID
});
accountSegmentsCollection.fetch({
success: function (collection, response, options) {
collection.currentPage = options.xhr.getResponseHeader('X-Current-Page');
}
});
return accountSegmentsCollection;
},
Someone stated below that I am not allowed to set attributes on the collection (except by way of the constructor, but that's before the fetch). So how else could I get this response header data into my view?
Collections in backbone js do not allow you to set attributes.
The best way to store meta information in a backbone collection is through plain javascript object.
For example:
var myCollection = Backbone.Collection.extend({
model: myModel,
metaInfo: {
currentPage: "2"
}
});
Then you can get the current page in your view using this.collection.metaInfo.currentPage
get in this context is a little confusing. In Backbone Models have attributes, but collections do not, this means that get on a collection is different from get on a model.
get on a model retrieves an attribute, while on a collection it will return a model (by id). You could extend your collection to have attribute getters and setters:
var pageCollection = Backbone.Collection.extend({
getAttribute: function(attr) {
return this.attributes?this.attributes[attr]:null;
},
setAttribute: function(attr,val){
this.attributes = this.attributes || {};
this.attributes[attr] = val;
}
});
You could then set your currentPage attribute:
pageCollection.setAttribute('currentPage', 'myPage');
And retrieve it:
App.log(pageCollection.getAttribute('currentPage');
Which should give you the functionality you are after.
So I'm trying to use a join table to display a list of data in my Parse app. The javascript API is similar enough to backbone.js that I'm assuming anyone who knows that could help me. I can't show my actual source code but I think I simple twitter-like "user follows user" scenario can answer my question. So assume I have a join table called "follows" that simply contains its own objectId, the id of each user in the relationship, and some meta-data about the relationship (needing metadata is why I'm using a join table, instead of Parse.Relation). I want to have a view that finds all of the users the current user follows and renders an instance of another view for each case. From what I have so far, that would looks something like this.
In the intialize of the top level view (let's call it AllFollowsView), I would have something like this.
var currentUser = Parse.User.current();
var followsQuery = new Parse.Query(Follows);
followsQuery.equalTo("userId", currentUser.id);
followsQuery.find({
success: function(followsResult){
for (var i = 0; i < followsResult.length; i++){
var view = new OneFollowView({model:followsResult[i]});
this.$("#followed-list").append(view.render().el);
}//for loop
},
error: function(error){
console.log("error finding plans query");
}
});
OneFollowsView is just a view that renders an showing data about the relationship and listens for changes on that particular relationship (mainly change or delete in my case). I understand that by passing in the corresponding model with
var view = new OneFollowView({model:followsResult[i]});
I can print out attributes of that model in the OneFollowsView template like this
<li>You are following a user with the id of <%= _.escape(followedUserId) %></li>
My problem is that this only gives me access to the information stored in the "follows" object. How would I pass in the corresponding user models (or any other models that I can query for the id of) into the template so I can access them in the html in the same way. I would like to be able to run queries in one of the views and then access those models in the html. I know I can add attributes to the object before declaring a new instance of the lower level class with that object as the model, but that doesn't help me because I don't want to save it with new attributes attached.
EDIT: My render function for the top level function is empty at the moment. It's initilize function contains this line to render the template. I guess this should probably be in the render function and then I would call render from initialize.
this.$el.html(_.template($("#all-follows-template").html()));
Here's the render for the lower (individual li) view
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
this.delegateEvents();
}
From my understanding this just renders the template to el while parsing the model to JSON and then returns to allow chained calls.
The problem here lies in you render method. When you call this.template in your render method. That method, this.template is a template function returned by calling the _.template function. When you call your this.template method, the properties of the object you pass in will be available as instance variables in your template.
In your case you're passing in the JSON of the object. So, the properties of the model become names of variables available in your template. If you want to expose additional variables to the template you have a couple options: 1) Add to the jsonified model's attributes. 2) Send in the model as a top level variable and any additional variables you may want.
// option 1
render: function() {
var templateArgs = _.extend(this.model.toJSON(), { additionalVar: 'new var' });
var content = this.template(templateArgs);
$(this.el).html(content);
this.delegateEvents();
return this;
}
// option 2
render: function() {
var templateArgs = {
followResult: this.model.toJSON(),
additionalVar: 'new var'
};
var content = this.template(templateArgs);
$(this.el).html(content);
return this;
this.delegateEvents();
}
Either option is reasonable. I would probably go with option 2. Which allows you in the template to say something like:
<li> <%= followResult.someProperty %> <%= additionalVar %> </li>
Hope that helps. :)
When instantiating a new view, I'm passing in a model. I have access to that model in the "initialize" property, but I can't reference it when I'm trying pass the model into a template. Any idea why?
var postView = Backbone.View.extend({
initialize : function() {
// returns model
console.log('the model we are interested in',this.model);
this.render();
},
el : "#blog-post",
template : function() {
// returns undefined
var model = this.model;
return _.template($('#postview').html(), {
post : model
});
},
render : function() {
var self = this;
this.$el.html(self.template);
}
});
I'm instantiating it using a method in another view:
readMore : function(e, index) {
var self = this;
var newView = new postView({
model : self.collection.models[index].toJSON()
});
}
You're passing a function to this.$el.html:
this.$el.html(self.template);
That's the same as saying:
var f = this.template;
this.$el.html(f);
So what does html do when you pass it a function? Well, from the fine manual:
.html( function )
A function returning the HTML content to set. Receives the index
position of the element in the set and the old HTML value as
arguments. jQuery empties the element before calling the function; use
the oldhtml argument to reference the previous content. Within the
function, this refers to the current element in the set.
When you pass html a function, it will call that function but this won't be what you think it is, this inside the function will be the DOM element that is having its HTML set.
I think you want to call this.template yourself and hand its return value to html:
this.$el.html(this.template());
// ------------------------^^
That way template will have the view as its this as you expect it to.
Best guess would be this no longer refers to the context of the view. if you log this within the function what does it show.
EDIT-
Actually not sure if this will give the expected results, i normally use handlebars but i think the setup for _.template and hanblebars is pretty similar. Your template template normally wants a plain java object passed to it other wise you would have to access the variables you want in yours like post.attributes.name, however if you just pass the toJSON version of your model you can access the attributes without the need for post.attributes.
Also you can compile your template once then just refer to it, no need place it as a function and have it grab from the DOM every time (assuming it never changes). Below is an example of what i mean. In your template you would then have <%= name %> etc to grab you model attributes.
var postView = Backbone.View.extend({
initialize : function() {
// returns model
console.log('the model we are interested in',this.model);
this.render();
},
el : "#blog-post",
template : _.template($('#postview').html()),
render : function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
Oh also the convention normally is that render returns 'this' so if you want to call it from some where else and attach it to a new part of the page you can do it calling postView.render().el
You may be passing the model, but you're not receiving it in your view. Try:
initialize: function(model) { ...
tl;dr
How to use backbone.stickit with a html form to change an existing model fetched from the server and only PATCH the changed attributes (changed by user input within the html form) to the server?
/tl;dr
I'm using backbone.stickit in a backbone.js application to bind a model to a HTML-form which is part of a backbone view. This works fine so far, but it becomes a little bit complicated if I'm going to save the bound model. This is because I want to use the PATCH-method and only send the changed attributes to the server. I try to illustrate what I've done so far:
Fetching the model from Server
user = new User(); //instatiate a new user-model
user.fetch(); //fetching the model from the server
console.log(user.changedAttributes()); // Returns ALL attributes, because model was empty
The last line indicates my problem, because I thought I can used the changedAtrributes() method later to get the attributes which need a patch on the server. So I tried this workaround which I found here
user.fetch({
success: function (model, response, options) {
model.set({});
}
});
user.changedAtrributes(); //Returns now "false"
Do stickit-bindings
Now I render my view and call the stickit() method on the view, to do the bindings:
//Bindings specified in the view:
[...]
bindings: {
"#username" : "username"
"#age" : "age"
}
[...]
//within the render method of the view
this.stickit();
The bindings work fine and my user model gets updated, but changedAttributes() remain empty all the time.
Save the model to the server
If the user has made all required changes, the model should be saved to the server. I want to use the PATCH method and only send the changed attributes to the server.
user.save(null, {patch:true}); //PATCH method is used but ALL attributes are sent to the server
OR
user.save(user.changedAttributes(),{patch : true});
With the second approach there are different outcomes:
if I didn't use the user.set({}) woraround, all attributes get PATCHED to the server
if I use the user.set({}) woraround the return value of changedAttributes() is "false" and all attributes are PUT to the server
if I call a user.set("age","123") before calling save(), then only the age attribute is PATCHED to the server
So outcome 3 is my desired behaviour, but there are 2 problems with this: First stickit doesn't seem to use the set() method on the model to update the attributes if they are changed within the html-form. And second, if you call set() with one attribute and afterwards with another, only the second attributes is returned by changedAttributes().
Maybe I just overseen something in the backbone or backbone.stickit docs, so I didn't get the desired behaviour working. Any ideas about that?
NOTE: As found out the problem wasn't directly related to backbone.stickit, more to backbone itself.
Solved this problem on my own, maybe this helps someone who may stumble upon this question:
Backbone only keep track of unchanged attributes, but not of unsaved attributes. So with
model.changedAttributes();
you will only get the attributes of the model, which was changed since the last
model.set("some_attribute","some_value")
Finally I stumbled upon backbone.trackit which is a backbone.js plugin maintained by the creator of backbone.stickit. With this plugin you can track unsaved attributes (all attributes which have changed since the last model.save()) and then use them in the save-method of a model. Example (my usecase):
Backbone.View.extend({
bindings: {
"#name" : "name",
"#age" : "age"
},
initialize: function () {
this.model = new User();
this.model.fetch({
success: function (model, response, options) {
//this tells backbone.stickit to track unsaved attributes
model.startTracking();
}
});
},
render: function () {
this.$el.html(tmpl);
this.stickit();
return this;
},
onSaveUserToServer: function () {
//first argument: only unsaved attributes, second argument: tell backbone to PATCH
this.model.save(this.model.unsavedAttributes(), { patch: true });
});
});