Issue with getJSON within Backbone view rendering - javascript

I'm pretty new to Backbone.js, loving it so far, but I'm having a little trouble trying to get relational data to render.
Within my Backbone view (called ImagesView) I have the following code:
// Render it
render: function () {
var self = this;
// Empty the container first
self.$el.html("")
// Loop through images
self.collection.each(function(img){
// convert `img` to a JSON object
img = img.toJSON()
// Append each one
self.$el.append(self.template(img))
}, self)
}
There are 3 images in the collection, and they are templated correctly with the above code. Within the img object is a user attribute, containing the User ID. I'm trying to return the user's details, and use these within the template instead of the ID. I'm doing that using the code below:
// Render it
render: function () {
var self = this;
// Empty the container first
self.$el.html("")
// Loop through images
self.collection.each(function(img){
// convert `img` to a JSON object
img = img.toJSON()
/* New code START */
// Each img has a `user` attribute containing the userID
// We'll use this to get their details
$.getJSON('/user/' + img.user, {}, function(json, textStatus) {
img.photographer = {
id: json.id,
username: json.username,
real_name: json.real_name
}
/* Moved 1 level deeper */
// Append each one
self.$el.append(self.template(img))
});
/* New code END */
}, self)
}
When I do this, the user's details are returned correctly and inserted into the template, but I now get 3 of each image returned instead of 1 (i.e. 9 in total), in a completely random order. What am I doing wrong? I'm open to using Backbone methods instead of the getJSON if that will make it easier, I just couldn't get it to work myself. I'm using underscore.js for the templating

The random order is likely caused by the requests being fired at very close intervals and responses returning out of the order they were fired in. I'm not sure why you're getting the multiple things, but if your template renders everything and you're calling that 3 times that could be it?
Anyway where I think you're going wrong is putting the responsibility of loading data into the render method. You'd want this to be abstracted so you have a good separation between data concerns and template concerns. As the ordering of the data is of interest, you'll want all 3 requests to have loaded before rendering. There's two different approaches you could take to this depending on if prior to loading this data you have sufficient data to render the view:
If you're waiting on all the data prior to rendering the view then you would want to render a different view (or template of this view) whilst the data is loaded and then replace that with a view showing all the data once it is loaded.
If you have sufficient data to render the view and what you are loading is supplementary, you'd want to render the view with the data you have in render and then once the other data is loaded use a custom method to modify the rendered view to include your data.
If you want to find out when all 3 requests are complete you can use http://api.jquery.com/jquery.when/

Related

Meteor: Data from External API call not rendering

I am relatively new to Meteor, and I'm trying to create a web store for my sister-in-law that takes data from her existing Etsy store and puts a custom skin on it. I've defined all of my Meteor.methods to retrieve the data, and I've proofed the data with a series of console.log statements... So, the data is there, but it won't render on the screen. Here is an example of some of the code on the server side:
Meteor.methods({
...
'getShopSections': function() {
this.unblock();
var URL = baseURL + "/sections?api_key="+apiKey;
var response = Meteor.http.get(URL).data.results;
return response;
}
...
});
This method returns an array of Object. A sample bit of JSON string from one of the returned Objects from the array:
{
active_listing_count: 20,
rank: 2,
shop_section_id: 1******0,
title: "Example Title",
user_id: 2******7
}
After fetching this data without a hitch, I was ready to make the call from the client side, and I tried and failed in several different ways before a Google search landed me at this tutorial here: https://dzone.com/articles/integrating-external-apis-your
On the client side, I have a nav.js file with the following bit of code, adapted from the above tutorial:
Template.nav.rendered = function() {
Meteor.call('getShopSections', function(err, res) {
Session.set('sections', res);
return res;
});
};
Template.nav.helpers({
category: function() {
var sections = Session.get('sections');
return sections;
}
});
And a sample call from inside my nav.html template...
<ul>
{{#each category}}
<li>{{category.title}}</li>
{{/each}}
</ul>
So, there's a few things going on here that I'm unsure of. First and foremost, the DOM is not rendering any of the category.title String despite showing the appropriate number of li placeholders. Secondly, before I followed the above tutorial, I didn't define a Session variable. Considering that the list of shop categories should remain static once the template is loaded, I didn't think it was necessary from what I understand about Session variables... but for some reason this was the difference between the template displaying a single empty <li> tag versus a number of empty <li>'s equal to category.length --- so, even though I can't comprehend why the Session variable is needed in this instance, it did bring me one perceived step closer to my goal... I have tried a number of console.log statements on the client side, and I am 100% sure the data is defined and available, but when I check the source code in my Developer Tools window, the DOM just shows a number of empty li brackets.
Can any Meteor gurus explain why 1) the DOM is not rendering any of the titles, and 2) if the Session variable indeed necessary? Please let me know if more information is needed, and I'll be very happy to provide it. Thanks!
You set the data context when you use #each, so simply use:
<li>{{title}}</li>
If a Session is the right type of reactive variable to use here or not is hard to determine without knowing what you are doing but my rough guess is that a Mini Mongo collection may be better suited for what it appears you are doing.
To get you started on deciding the correct type of reactive variable to use for this head over to the full Meteor documentation and investigate: collections, sessions, and reactive vars.
Edit: To step back and clarify a bit, a Template helper is called a reactive computation. Reactive computations inside of helpers will only execute if they are used in their respective templates AND if you use a reactive variable inside of the computation. There are multiple types of reactive variable, each with their own attributes. Your code likely didn't work at all before you used Session because you were not using a reactive variable.

Meteor JS: Timing of Accessing MongoDB affects the result of the Access

I have a collection in my Meteor JS app:
MenuItems = new Mongo.Collection('menu_items');
In my Template Helper file I access this collection:
Template.admin_menu_items.helpers({
menuItems: function(){
//return items from DB
console.log('inside menuItems');
snapshot = MenuItems.find().fetch();
console.log(snapshot);
return snapshot;
},
});
Then in my html template file I call this helper:
{{#each menuItems}}
{{#each items}}
{{this}}
{{/each}}
{{/each}}
Then in my Template rendered helper callback section I again access that collection:
Template.admin_menu_items.rendered = function(){
console.log('Template.admin_menu_items.rendered');
var snapshotRendered = MenuItems.find().fetch();
//
console.log(snapshotRendered);
}
If I refresh the page I get the following output:
inside menuItems admin_m...4d3e6ec (line 9)
[] admin_m...4d3e6ec (line 13)
Template.admin_menu_items.rendered admin_m...4d3e6ec (line 36)
[] admin_m...4d3e6ec (line 40)
inside menuItems admin_m...4d3e6ec (line 9)
[Object { _id="nHZBfwAt64dwiPjCB", items=[3]}]
What I would like to understand and ask about is: why is it that the first call to MenuItems.find().fetch() inside the menuItems helper and the first call to MenuItems.find().fetch() inside the Template rendered callback function returns a empty [] array when the MenuItems collection has documents inside it already???
Is it because my template and template helper files are deeper in the hierarchy of files and therefore is loaded earlier than my menu_item.js file that instantiates the MenuItems variable by:
MenuItems = new Mongo.Collection('menu_items');
How do I ensure that the MongoDB will return the right number of documents from the very initial call or access done to it???
Thank you very much
This is more to do with subscriptions and data-over-the-wire than it is to do with the load order of files. When you refresh the page, you get the template first and the data later, so the order of operations is something like this:
The template starts building itself without data, so the helper runs with an empty array
The template is finished building and renders, but without data it too has an empty array
The data arrives
The helper reactively re-runs with the new data, and returns a full array
The solution to this, if you need the data in the rendered callback, is to prevent this template from running before the data arrives. You can handle this with a router that waits on a subscription, or by checking the data and only rendering the template once the number of items is not zero, or by using the Template.subscribe function and guarding your template with an if statement until the data is ready.
On a side note, you may want to return the result of find() in your helper rather than find().fetch(), it'll work the same in an {{#each}} block, but with finer-grained reactivity as Blaze can iterate over the cursor itself. Unless of course you really need the array.
How do I ensure that the MongoDB will return the right number of documents from the very initial call or access done to it???
Well, in the Meteor spirit... You don't.
The whole thing about Meteor is reactivity. You don't want to have to wait for the data to arrive to start rendering. It would mean that your user would be watching at a blank page for a while before seeing some action, because the page blocks rendering, waiting in dark oblivion for the full data.
Instead you go for reactivity. You execute reactive computations which are executed immediately, then each time reactive data inside them updates. Helpers are reactive computations, Mongo cursors in Meteor are reactive data. Helpers are run initially (maybe returning nothing because the data hasn't arrived yet), then each time some new data comes in.
You have to take the case of "Nope, no data yet" into account when coding these reactive computations. For example, if(someCollection.findOne()) would make sure that there is at least one document in someCollection.
In your case it may not really be useful though since Spacebars handles cursors pretty well.

Collection sorting with comparator only works after fetch is complete?

I have a simple collection of messages that I want to reverse sort on time (newest on top), using comparator:
...
this.comparator = function(message) {
var time = new Date(message.get("time")).getTime();
return time;
}
...
In my view, I use fetch and add event:
messages = new MessageCollection();
messages.fetch({update: true});
messages.on("add", this.appendMessage);
...
appendMessage: function(message) {
var messageView = new MessageView({
model: message
});
this.$el.prepend(messageView.render().el);
}
Sadly, the messages are not rendered in the order I am looking for, but in the original order they were in coming from the server.
Now, after some testing I found out that when I add all the messages at once (using reset), the order is as I expected.
messages.fetch();
messages.on("reset", this.appendCollection);
...
appendCollection: function(messages) {
messages.each(function(message) {
this.appendMessage(message);
}, this);
}
Even though I can understand this process since a collection probably can only figure out how it's supposed to be sorted after all models are added, this (the on("add") configuration) used to work in Backbone 0.9.2.
Am I missing something? Did the comparator method change, or the event model in regard to add? Or am I going at it the wrong way? Thanks!
You call appendMessage method when you add a model in collection. the appendMessage is being called in the order of adding models and not the actual order in the collection.
In the "add" case, the model is inserted in the right position in the collection, as it should be by "comparator" documentation). But then you are doing
this.$el.prepend(messageView.render().el);
which will put the html from the MessageView rendering at the top of the $el (which I assume is the CollectionView container).
The best way to also keep the Html respecting the sorted order would be to re-render the collection view, or scroll the collection view children and insert the added messageView at the right place (a bit more difficult to do).

How do I serialize data from non-form elements and make it accessable from params[:model]

I have a Sinatra app which loads information from an external API and displays it on a page. This is done in Sinatra which gets the information and puts it a temporary model instance (which is NOT saved), so it is easier to access all its propertys in the view.
Now when the user clicks a link I want the model instance to be saved to the database, which I think only can be done via AJAX etc. because the last request already finished and none of the instances is still alive. I thought I needed to extract all the information of the corresponding HTML elements and make an AJAX-Post to another route.
My problem is now, I want to be able to create(and save) the model using #model = Model.create(params[:model]). It would be clear what to do using a form, but that is not an option because all the data is displayed within a table and each table row is one instance of the model.
How do I serialize the data from the table row in which the clicked link is, so I can use it as described above?
UPDATE
I am using MULTIPLE instances of the object class, each in one tablerow!
I am using DataMapper, only the temporary objects are not stored!
I dont want to clutter up my whole setup!
Did you consider ActiveResource? You can use ActiveResource to maintain object state. If your REST API follows convention it would be very easy to map resource.
Regarding second half sending back data to your controller, you could store in hidden variable(s) and on post it should be easy to construct back the object and persist it to database.
Something like
#model
class MyModel < ActiveResource::Base
# set configs here
end
# To fetch record from REST API in controller or whatever
MyModel.find(1)
#in controller on form submit or AJAX
post "/path" do
MyModel.new(params[:myModel])
end
Update
To maintain state of object without using hidden form
in javascript you can have something like
var myModel = #{myModel.to_json}; #Ruby interpolation in HAML it will depend on templating language
on certain action you can update the JSON object
and to post using AJAX
$.post("post/path", myModel);
More Update
In External JS
function my_js_function(obj) {
/* do something useful here like setting up object hash etc
*/
}
In Ruby Template
<script>
var myObj = #{myObj.json}
my_js_function(myObj);
</script>
I found a pretty easy solution. It was nothing more than getting all the required values from the DOM and putting them into an Array!
application.js:
$(".enable").click(function() {
var table_row = $(this).closest("tr");
var model_array = new Array;
var elements_with_information = jRow.find("[name]");
elements_with_information.each(function() {
// Doing some checking on which kind of element
// it actually is and then basically doing:
model_array.push($(this).text());
});
// Constructing nested array to use `params[:model]`
var data = { "model" : {
"property1": model_array[0],
"property2": model_array[1]
}};
// Now doing the AJAX request
$.ajax({
url: "/model/doshit",
type: "POST",
data: data
});
});

backbone.js cache collections and refresh

I have a collection that can potentially contain thousands of models. I have a view that displays a table with 50 rows for each page.
Now I want to be able to cache my data so that when a user loads page 1 of the table and then clicks page 2, the data for page 1 (rows #01-50) will be cached so that when the user clicks page 1 again, backbone won't have to fetch it again.
Also, I want my collection to be able to refresh updated data from the server without performing a RESET, since RESET will delete all the models in a collection, including references of existing model that may exist in my app. Is it possible to fetch data from the server and only update or add new models if necessary by comparing the existing data and the new arriving data?
In my app, I addressed the reset question by adding a new method called fetchNew:
app.Collection = Backbone.Collection.extend({
// fetch list without overwriting existing objects (copied from fetch())
fetchNew: function(options) {
options = options || {};
var collection = this,
success = options.success;
options.success = function(resp, status, xhr) {
_(collection.parse(resp, xhr)).each(function(item) {
// added this conditional block
if (!collection.get(item.id)) {
collection.add(item, {silent:true});
}
});
if (!options.silent) {
collection.trigger('reset', collection, options);
}
if (success) success(collection, resp);
};
return (this.sync || Backbone.sync).call(this, 'read', this, options);
}
});
This is pretty much identical to the standard fetch() method, except for the conditional statement checking for item existence, and using add() by default, rather than reset. Unlike simply passing {add: true} in the options argument, it allows you to retrieve sets of models that may overlap with what you already have loaded - using {add: true} will throw an error if you try to add the same model twice.
This should solve your caching problem, assuming your collection is set up so that you can pass some kind of page parameter in options to tell the server what page of options to send back. You'll probably want to add some sort of data structure within your collection to track which pages you've loaded, to avoid doing unnecessary requests, e.g.:
app.BigCollection = app.Collection.extend({
initialize: function() {
this.loadedPages = {};
},
loadPage: function(pageNumber) {
if (!this.loadedPages[pageNumber]) {
this.fetchNew({
page: pageNumber,
success: function(collection) {
collection.loadedPages[pageNumber] = true;
}
})
}
}
});
Backbone.Collection.fetch has an option {add:true} which will add models into a collection instead of replacing the contents.
myCollection.fetch({add:true})
So, in your first scenario, the items from page2 will get added to the collection.
As far as your 2nd scenario, there's currently no built in way to do that.
According to Jeremy that's something you're supposed to do in your App, and not part of Backbone.
Backbone seems to have a number of issues when being used for collaborative apps where another user might be updating models which you have client side. I get the feeling that Jeremy seems to focus on single-user applications, and the above ticket discussion exemplifies that.
In your case, the simplest way to handle your second scenario is to iterate over your collection and call fetch() on each model. But, that's not very good for performance.
For a better way to do it, I think you're going to have to override collection._add, and go down the line dalyons did on this pull request.
I managed to get update in Backbone 0.9.9 core. Check it out as it's exactly what you need http://backbonejs.org/#Collection-update.

Categories