I'm trying to build a simple Backbone app from this post Unable to fetch data from Twitter RESTful API using Backbone.js Collection . What I wanna do is adding a text box and a button. Every time a user press a button, my app will request to twitter and will search in twitter based on the textbox content (it will search the word 'NYC' as default).
My code is as follows.
My tweets collection
var Tweets = Backbone.Collection.extend({
model: Tweet,
url: 'http://search.twitter.com/search.json?q=NYC&callback=?',
parse: function(response) {
console.log('parsing ...')
return response.results;
}
});
My view.
var PageView = Backbone.View.extend({
el: $('body'),
events: {
'click button#add': 'addItem'
},
initialize: function() {
_.bindAll(this, 'render', 'addItem');
this.tweets = new Tweets();
this.counter = 0;
this.render();
},
render: function() {
$(this.el).append("<input id= 'search'type='text' placeholder='Write a word'></input>");
$(this.el).append("<button id='add'>Search twitts</button>");
$(this.el).append("<ul id='tweets'></ul>");
return this;
},
addItem: function(item) {
var subject = $('#search').val() || 'NYC';
this.tweets.url = 'http://search.twitter.com/search.json?q=' + subject + '&callback=?';
// never called ...
this.tweets.bind('reset', function(collection) {
console.log(collection.length);
alert(collection.length);
});
$.each(this.tweets.fetch().models, function(index, tweet) {
item = new Tweet();
item.set({
order: this.counter,
info: tweet.attributes.text// modify item defaults
});
$('ul', this.el).append("<li>" + item.get('order') + " " + item.get('info') + "</li>");
});
}
});
var pageView = new PageView();
The thing is:
First click (empty textbox) : twitter request is captured by parse
within Tweets collection. Nothing is appended to my ul element.
Second click (with some word): twitter request is made with that
word. However, 'NYC' tweets from the previous call are printed.
Third click (with some other word): 'Some word' from second click
are printed.
....
I realize that everytime I try to print my tweets I get the previous reply because Twitter hasn't replied me yet. I suppose that I could get my tweets in the parse callback and printed them, but it seems quite nasty.
On the other hand I've tried to get my tweets binding them to the reset event, but it doesn't work either.
Any ideas??.
Thanks in advance!!
Here's your example code done in a more idimatic Backbone pattern.
Basically:
You have your Tweets collection
You bind an event handler to the collection's reset event that does something with the newly populated collection
In your view, you set up an event handler for your button-click event that calls collection.fetch() to get new data from the 'server'.
When the fetch is completed, the reset event is triggered, calling the event handler bound to the reset event.
Code:
var Tweet = Backbone.Model.extend();
var Tweets = Backbone.Collection.extend({
model: Tweet,
url: 'http://search.twitter.com/search.json?q=NYC&callback=?',
parse: function(response) {
console.log('parsing ...')
return response.results;
}
});
var PageView = Backbone.View.extend({
el: $('body'),
events: {
'click button#add': 'doSearch'
},
initialize: function() {
_.bindAll(this, 'render', 'addItem');
this.tweets = new Tweets();
_this = this;
// Bind an event handler for the reset event.
this.tweets.bind('reset', function(collection) {
// Clear any old tweet renderings
_this.$('#tweets').empty();
// For each tweet in the collection, we call addItem and
// and pass the tweet.
collection.each(function(tweet) {
_this.addItem(tweet);
});
});
this.counter = 0;
this.render();
},
// This function is the event handler for the button click.
// It tells the Tweets collection to fetch()
doSearch: function() {
var subject = $('#search').val() || 'NYC';
this.tweets.url = 'http://search.twitter.com/search.json?q=' + subject + '&callback=?';
this.tweets.fetch();
},
render: function() {
$(this.el).append("<input id= 'search'type='text' placeholder='Write a word'></input>");
$(this.el).append("<button id='add'>Search twitts</button>");
$(this.el).append("<ul id='tweets'></ul>");
return this;
},
addItem: function(item) {
console.log(item);
$('ul', this.el).append("<li><b>" + item.get('from_user_name') + "</b>: " + item.get('text') + "</li>");
}
});
var pageView = new PageView();
Live example to play with: http://jsfiddle.net/38L35/16/
Related
I am new to backbone and working on a CMS built in backbone. The CMS allows the creators to add new homepages for the website. The size of the response for 50 items from the API is about 25mb. My HTML page has a load button which fires a event and loads 50 more items when it is clicked. The problem that I am facing is that the view takes some time to load when ever the click is fired i.e the view disappears and the user forgets where he was before the loading of the view. Is there a way to make it make the view rendering much faster so it does not disappear and the user can remember where exactly he was. The event to load more data is fired when button with class load-more is clicked.
Here is some part of my code.
The view:
cms.views.Homepages = Backbone.View.extend({
events: {
'click .filter li': 'filterArticles',
'click .list-column-title' : 'openLink',
'click .homepageAction': 'updateHomepageStatus',
'click .more-actions .delete': 'deleteHomepage',
'click .more-actions .duplicate': 'duplicate',
'click .editPubDate': 'editPubDate',
'click .next': 'nextItems',
'click .prev': 'prevItems',
'click .load-more': 'loadMore'
}, initialize: function(homepages, articles) {
this.template = new EJS({url: 'app/views/root/homepages/homepages.template.ejs'});
this.homepagesTableTemplate = new EJS({url: 'app/views/root/homepages/homepages-table.template.ejs'});
this.listenTo(cms.data.homepages, 'loaded', this.displayTable,this.render);
cms.data.homepages.load();
this.listenTo(cms.data.homepages, 'nomoreload', this.disableMoreLoad)
this.initDialogs();},
render: function(options) {
var html = this.template.render({homepages: cms.data.homepages.toJSON()});
this.$el.html(html);
return this;
},
loadMore: function(e) {
e.preventDefault();
this.collection.loadMore();
//this.collection.reset(newmodel.loadMore());
console.log( this.collection);}
The code in my collection
cms.collections.HomePages = Backbone.Collection.extend({
model: cms.models.HomePage,
currentPage: 0,
url: function () {
//console.log(this, this.currentPage )
return cms.config.BASE_URL + 'cms/homepage?page=' + this.currentPage + '&per_page=50&filter[sort]=creationdate_desc'
},
loadMore: function() {
this.currentPage ++;
var self = this;
this.fetch({
success: function(collection, response) {
self.trigger('loaded', null);
// if response less than 50 -> end of load
console.log(collection);
if (response.length === 0) {
self.trigger('nomoreload', null);
}
},
error: function() {
self.trigger('loaded', true);
},
remove: false
});
As Emile Bergeron rightly mentioned, 25 MB is a lot of data on a homepage. In this situation, you will have to optimize your code so as to get response in chunks and show only what is required to the user.
Another approach is store a certain amount of data in a Web worker so you can provide a chunk of that data directly from there avoiding a network call. Fetch new data as and when required and store it in your web worker by clearing the previous stale data.
I'm novice in Backbone.
I want to show a stock list, where user can open up any stock right from the list and change stock values. After that the whole list should refresh to show changed values.
So as I found out it's better not only to create collection but create collection and a list of stock models.
For this I created a stock collection view for main table and stock model view for adding rows to the table where each row is a single model.
So this is a collection view:
App.Views.StockTable = Backbone.View.extend({
...
initialize: function() {
this.render();
},
render: function() {
this.$el.html(this.template(this.collection));
this.addAll();
return this;
},
addOne: function(stock) {
var row = new App.Views.StockRow({
model: stock,
suppliers: this.suppliers
});
return this;
},
addAll: function() {
var suppliers = new App.Collections.Suppliers();
var that = this;
suppliers.fetch({
success: function() {
_.each(that.collection.toJSON(), that.addOne, that);
}
});
return this;
}
});
And this is my stock row view:
App.Views.StockRow = Backbone.View.extend({
el: 'tbody',
templateRow: _.template($('#stockRow').html()),
templatePreview: _.template($('#stockPreview').html()),
events: {
'click #open': 'open'
...
},
initialize: function() {
this.render();
},
render: function() {
this.$el.append(this.templateRow(this.model))
.append(this.templatePreview({
stock: this.model,
suppliers: this.suppliers
}));
return this;
},
open: function(e) {
var element = $(e.currentTarget);
element.attr('id', 'hide');
$('#stock' + element.data('id')).slideToggle('fast');
}
...
});
I wrote just a piece of code. The problem is that when I click on '#open' that event triggers many times (right the quantity elements in the collection). So when I catch e.currentTarget there are many similar objects.
What i do wrong?
I suspect you have multiple things going on here.
Without seeing your template, I suspect each of your StockRow rows are rendering a tag with the id="open". Since id values should be unique, use a class in your link (example: class="open"), and then reference that class in your click handler:
events: {
'click .open': 'open'
}
Next, since each instance of the StockRow already has a model instance associated with it, just use this.model instead of trying to look it up out of the data attribute of the currentTarget.
open: function () {
$('#stock' + this.model.id).slideToggle('fast');
}
But again, instead of using an id="stock" attribute in your template, use a class… say class="stock-preview". Then just look for that in your open…
open: function () {
this.$el.find('.stock-preview').slideToggle('fast');
}
The other piece that looks questionable to me is the call to this.addAll(); in the render method of the StockTable view. It is best practice to just have your render method render state, instead of having it trigger an ajax call to fetch the state.
For example, in your initialize you can setup some event handlers that react to your collection changing state (below is an incomplete example, just hoping to get you going in the right direction):
initialize: function (options) {
…
_.bindAll(this, 'render', 'renderRow');
this.collection.on('add', this.renderRow);
this.collection.on('reset', this.render);
},
render: function () {
this.$el.html(this.tableTemplateWithEmptyTBodyTags());
this.collection.each(this.renderRow);
return this;
},
renderRow: function () {
var row = new App.Views.StockRow({
model: stock,
suppliers: this.suppliers
});
this.$el.find('tbody').append(row.render().el);
return this;
}
And then outside the table view, you can do a suppliers.fetch(). Which when the response comes back should trigger the reset.
I'm new to Backbone. I have a collection whose url function depends on the textfield text. How do i get that text from my textfield. No i don't want to use JQuery selectors as accessing outside selectors from your views aint a good practice. My HTML stucture is like:
<div id='outer'>
<input type='text' id='xyz'>
<div id='image123'></div>
<div id='div1'>
<ul>
</ul>
</div>
<div id='div2'></div>
</div>
So i got 2 views, 1 collections & 1 model.
How do i get the input text in the collection without using JQuery selectors from my 'outer' view.
[Post updated with View code]
var outerView = Backbone.View.extend
({
el: '#outer',
initialize: function()
{
},
events:
{
'keyup #xyz' : 'keyfunc'
},
keyfunc: function()
{
// inputtext is a global variable & i don't want it that way
inputtext = $('#xyz').val();
},
render: function()
{
},
});
I couldn't figure what are you doing , but only if you want to send the value to the collection with out making it global and changing the url ,try doing it this way
var outerView = Backbone.View.extend
({
el: '#outer',
initialize: function()
{
},
events:
{
'keyup #xyz' : 'keyfunc'
},
keyfunc: function()
{
// not global now
var inputtext = $('#xyz').val();
var myclooction = new MainCollection({
text : inputtext
});
},
render: function()
{
},
});
in the collection
var MainCollection = Backbone.extend.collection({
url : function(){
return "someurl/"+this.text;
},
// receive the value here
initialize:function(options){
this.text = options.text;
}
});
Backbone passes information about the element that triggered the event, and there you can find that value like so:
keyfunc: function(e)
{
inputtext = $(e.currentTarget).val();
this.model.trigger('textChanged', {id: this.myID, data: inputtext});
}
and your model can listen that event like this in its initialize-function.
this.listenTo(this, 'textChanged', this.textChangedHandler);
And the model can then decide what to do with that event. For example to send it to that another view by another event.
textChangeHandler: function (e) {
this.trigger('someTextChanged', e);
}
And your views or collections can listen that event in their initialize-function:
this.listenTo(this.model, 'someTextChanged', this.textChangedHandler);
And the handler would be something like:
textChangeHandler: function (e) {
if (e.id !== this.myID) {
//do stuff
}
}
I'm working with an API and Backbone.js at the moment.
I have two views, both render to the same document element #viewContainer. Both of these views render a table with a couple strings to decribe them and a button that opens a form in a modal.
View 1
App.Views.TaskList = Backbone.View.extend({
el: "#viewContainer",
tagName: 'tr',
events: {
"click button": "showTaskForm"
},
showTaskForm: function (event) {
event.preventDefault();
var id = $(event.currentTarget).data("id");
var item = this.collection.get(id);
var formView = new App.Views.Form({
model: item
});
formView.render();
},
render: function () {
changeActive($('#tasksLink'));
var template = _.template($("#taskList").html(), {});
$('#viewContainer').html(template);
// loop and render individual tasks.
this.collection.each(function (model) {
var variables = {
name: model.get('name'),
button: model.getButton()
};
var template = _.template($("#task").html(), variables);
$("#taskTable tbody").append(template);
});
},
collection: App.Collections.Tasks,
});
View 2
App.Views.ProcessList = Backbone.View.extend({
el: "#viewContainer",
tagName: 'tr',
events: {
"click button": "showStartForm"
},
showStartForm: function (event) {
event.preventDefault();
var id = $(event.currentTarget).data("id");
var item = this.collection.get(id);
var formView = new App.Views.Form({
model: item
});
formView.render();
},
collection: App.Collections.Processes,
render: function () {
changeActive($('#processLink'));
var template = _.template($("#processList").html(), {});
$('#viewContainer').html(template);
this.collection.each(function (model) {
var variables = {
processId: model.get('id'),
processName: model.get('name'),
button: model.getButton()
};
var template = _.template($('#process').html(), variables);
$('#processList tbody').append(template);
});
} });
Neither of these views are rendered by default, both need to be activated by a button on the page and they over-write each other in the DOM. However, which ever view is rendered first, the click event of the buttons in that view are the ones that are always fired.
If there is any more information needed from me let me know and I will edit the question.
Be sure to call undelegateEvents() in the first view when you render the second.
Since you're listening for events on the same elements, essentially you attached two listeners for click events on the same button, and when you change your views you are not cleaning up these listeners.
Here's an article that talks about managing events on view change, which should be really helpful to you.
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
As other posters have pointed out, you need to watch out for 'zombie' views (i.e. making sure you undelegate events). If you're building even a moderately complex app, you'll want something that can scale. I find this pattern useful:
var BaseView = Backbone.View.extend({
render: function () {
this.$el.html(this.template());
return this;
},
close: function () {
if (this.onClose) this.onClose();
this.undelegateEvents();
this.$el.off();
this.$el.remove();
}
});
Then whenever you build a view you can do:
var view = BaseView.extend({
//your code
//now the .close() method is available whenever you need to close
//a view (no more zombies!).
});
I'm trying to grok Backbone a little more, and from someone who has only used Backbone views in the past, I'm now trying my hand with Models and Collections.
Right now, when I post a comment, I try to increment the comment count.
Model:
Comment = Backbone.Model.extend({
defaults: {
text: null,
count: 0
},
updateCount : function() {
console.log(this.set('count', this.get('count') + 1));
console.log(this.get('count'));
}
});
Collection:
CommentsCollection = Backbone.Collection.extend({
model: Comment,
initialize: function (models, options) {
this.on("add", options.view.appendComment);
this.on('add', options.view.resetComment);
}
});
View:
CommentsView = Backbone.View.extend({
el: $("body"),
initialize: function () {
_.bindAll(this,
'addComment',
'appendComment',
'resetComment'
);
this.comments = new CommentsCollection(null, {
model: Comment,
view: this
});
},
events: {
"click #post-comment": "addComment"
},
addComment: function (evt) {
var $target = $(evt.currentTarget);
var $container = $target.closest('#comment-wrapper');
var text = $container.find('textarea').val();
var comment = new Comment({
text: text
});
//Add a new comment model to our comment collection
this.comments.add(comment);
return this;
},
appendComment: function (model) {
$('#comments').prepend('<div> ' + model.get('text') + '</div>');
model.updateCount();
return this;
},
resetComment: function () {
$('textarea').val('');
}
});
Why is it always returning 1 (add a comment and click Post then view the console to see)?
Demo: http://jsfiddle.net/ZkBWZ/
This is happening because you're storing the count on the Comment model. Each time you hit the submit button, you create a new Comment which has the count set to the default, 0. The method updateCount then updates the count on that brand new model, so you're always seeing 1.
If you're just looking to determine how many comments have been made, I'd suggest you just look at the size of the CommentsCollection. In appendComment, you can do it this way:
appendComment: function (model) {
$('#comments').prepend('<div> ' + model.get('text') + '</div>');
// Get the number of comments
console.log(model.collection.models.length);
return this;
},