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;
},
Related
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 trying to learn Backbone.js and for this I now want to load a collection of models into a view. By opening a tab in the window, I first add the following template:
<script type="text/template" id="tab-content-template">
<div class="conversation-window" id="conversation<%= ticketId %>"></div>
</script>
In this tempalte I now want to load a Collection of messages belonging to the ticketId. So I made a collection like this:
var MessageCollection = Backbone.Collection.extend({
url: 'ticket/:id/messages'
});
and a view:
var MessageView = Backbone.View.extend({
initialize: function(models, options) {
this.ticketId = options.ticketId;
},
el: function() {
return '#conversation' + this.ticketId;
},
className: 'user-message'
});
So I want the list of messages to be inserted within the #conversation1 (for ticketId 1). I then tried running this:
var messageView = new MessageView(messageCollection, {ticketId: 1});
messageView.render();
console.log(messageView);
Unfortunately nothing happens, and when I look into the console I see that ticketId: 1 but that el: undefined. I'm kinda lost in what I'm doing wrong (kinda lost in Backbone in general).
Does anybody know what I'm doing wrong here and how I can solve it? All tips are welcome!
I think this is what you want:
<div id = "conversation1">stuff insert here</div>
<script>
var MessageCollection = Backbone.Collection.extend({
// url: 'ticket/:id/messages' //<-- put this in router
});
// you need to create an instance of you collection somewhere and pass in
// models as parameter. note that your ticketId is included in the models:
var messageCollection = new MessageCollection([{ticketId:1,attr:'stuff1'}, {ticketId:2,attr:'stuff1'}])
var MessageView = Backbone.View.extend({
initialize: function(models, options) {
this.ticketId_1 = this.collection.models[0].get('ticketId');
this.ticketId_2 = this.collection.models[1].get('ticketId');
},
// can not reference el and define a <div id="user-message"> simultaneously
/*
el: function() {
return '#conversation' + this.ticketId;
},
*/
className: 'user-message',
render: function() {
$('#conversation'+ this.ticketId_1).html(this.$el.html('stuff from model goes in here'));
}
});
// var messageView = new MessageView( messageCollection, {ticketId: 1});
// the above wouldn't work. your ticketId should be passed into your view via model
// then you associate you view to your collection like so:
var messageView = new MessageView({ collection: messageCollection });
messageView.render();
console.log(messageView.$el.html());
</script>
I have 2 textfields with id's source,destination. If any field value changes that corresponding model attribute will be change. I did this one using Backbone.Model and events object in Marionette.CompositeView. It's working fine.
Once any model Attribute change corresponding function will call. For this I written the following code. It's not working the problem was even one attribute changes both functions are evaluating.
model Code:
var mapModel = Backbone.Model.extend({
defaults: {
startPlace: "",
endPlace: ""
}
});
Marionette.CompositeView code:
var mapView = Marionette.CompositeView.extend({
events: {
"blur #source": "sAttributeSetting",
"blur #destination": "dAttributeSetting"
},
dAttributeSetting: function() {
this.model.set({"endPlace": document.getElementById(this.ui.destinationPlace).value});
},
sAttributeSetting: function() {
this.model.set({"startPlace": document.getElementById(this.ui.sourcePlace).value});
},
modelEvents: {
"change startPlace": "startMarkerDisplay",
"change endPlace": "endingMarkerDisplay"
},
startMarkerDisplay: function() {
alert("start");
},
endingMarkerDisplay: function() {
alert("end");
}
});
html code:
<input type="text" id="source">
<input type="text" id="destination">
creating instance for both model and view
mapModelObj = new mapModel();
var mapViewObj = new mapView({el:$('#mapDiv'), model:mapModelObj});
problems:
Initially If I enter any value in first field(source) getting 2 alert boxes("start", "end").
Initially If you enter any value in second field(destination) getting 4 alert boxes("start", "end", "start", "end")
I tried alot but I didn't understand where I am getting the problem
Can anyone help me.
Thanks
modelEvents should be connected by :. Say, event of changing startPlace should be
'change:startPlace'
If you use space you'll end with two events, not one event specific to this attribute.
Your code 'change startPlace' represents two events, one is 'change', the other is 'startPlace'. So you'll see "start","end","start","end"
My observations are the following for your solution (however I propose a second solution at the bottom):
The binding of entity event has colon syntax. It should be a hash of { "event:name": "eventHandler" } configuration. Multiple handlers can be separated by a space. A function can be supplied instead of a string handler name.
You can use advantage of the el property of the backbone view.
Instead of using document.getElementById(this.ui.sourcePlace), you can use this.$('#source'). This latest searches only in the context of el rather than searching the whole dom. This way the evaluation will be way faster... That way you should use this expression: this.$('.destination').val()
Please check my jsfiddle about your issue: http://jsfiddle.net/orbanbotond/VEcK6/
The code is the following:
var mapModel = Backbone.Model.extend({
defaults: {
startPlace: "",
endPlace: ""
}
});
var mapView = Marionette.CompositeView.extend({
events: {
"blur .source": "sAttributeSetting",
"blur .destination": "dAttributeSetting"
},
dAttributeSetting: function(){
console.log('end blured');
console.log('input value:' + this.$('.destination').val());
this.model.set({
"endPlace": this.$('.destination').val()
});
console.log('endplace set to: ' + this.model.get('endPlace'));
},
sAttributeSetting: function() {
console.log('start blured');
console.log('input value:' + this.$('.source').val());
this.model.set({
"startPlace": this.$('.source').val()
});
console.log('startPlace set to: ' + this.model.get('startPlace'));
},
modelEvents: {
"change:startPlace": "startMarkerDisplay",
"change:endPlace": "endingMarkerDisplay"
},
startMarkerDisplay: function () {
alert("start");
},
endingMarkerDisplay: function () {
alert("end");
}
});
$(document).ready(function(){
var mapModelObj = new mapModel();
var mapViewObj = new mapView({
el: $('#mapDiv'),
model: mapModelObj
});
});
My proposed second solution:
Use the stickit library which does all you are doing. You only need to define the mapping between the dom selector and the observed model attribute.
Here is the jsfiddle for it: http://jsfiddle.net/orbanbotond/fm64P/
Here is the code:
var mapModel = Backbone.Model.extend({
defaults: {
startPlace: "initialStartPlace",
endPlace: "initialEndplace"
},
});
var mapView = Marionette.CompositeView.extend({
template: "#mapDiv",
events: {
"blur .source": "sAttributeSetting",
"blur .destination": "dAttributeSetting"
},
bindings: {
'.source': {
observe: 'startPlace'
},
'.destination': {
observe: 'endPlace'
}
},
onRender: function() {
this.stickit();
console.debug("Sticked to it already");
},
});
$(document).ready(function(){
var mapModelObj = new mapModel();
var mapViewObj = new mapView({
el: $('#mapDiv'),
model: mapModelObj
});
mapViewObj.render();
mapModelObj.bind('change:startPlace', function(obj){alert("New value: " + obj.get('startPlace'));});
mapModelObj.bind('change:endPlace', function(){alert("New value: " + obj.get('endPlace'));});
});
For every code sample I used this template (I used class selectors instead of id selectors):
<div id="mapDiv">
<input type="text" class="source">
<input type="text" class="destination">
</div>
I'm having trouble using Marionette's CompositeView. I render my model in my CompositeView using a template and want to add a click event to it. Somehow I can't get the events to work using the events: { "click": "function" } handler on the CompositeView... What am I doing wrong?
var FactsMenuItem = Backbone.Marionette.ItemView.extend({
template: tmpl['factsmenuitem'],
initialize: function() {
console.log('factsmenuitem');
},
onRender: function() {
console.log('factsmenuitem');
}
});
var FactsMenuView = Backbone.Marionette.CompositeView.extend({
template: tmpl['factsmenu'],
itemView: FactsMenuItem,
itemViewContainer: ".subs",
events: {
'click': 'blaat'
},
blaat: function() {
console.log('this is not working');
},
initialize: function() {
this.model.get('pages').on('sync', function () {
this.collection = this.model.get('pages');
this.render();
}, this);
},
onRender: function() {
console.log('render factsmenu');
}
});
var FactsLayout = Backbone.Marionette.Layout.extend({
template: tmpl['facts'],
regions: {
pages: ".pages",
filter: ".filter",
data: ".data"
},
initialize: function(options) {
this.currentPage = {};
this.factsMenu = new FactsMenu();
this.factsView = new FactsMenuView({model: this.factsMenu});
},
onRender: function() {
this.pages.show(this.factsView);
}
});
Edit:
I removed some code that made the question unclear...
The problem lies that the events of the non-collectionview of the compositeview (the modelView??) are not fired. I think this has something to do with the way the FactsLayoutView instantiates the compositeview...
The problem was caused by the way the region was rendered. In my FactsLayout is used this code:
initialize: function(options) {
this.currentPage = {};
this.factsMenu = new FactsMenu();
this.factsView = new FactsMenuView({model: this.factsMenu});
},
onRender: function() {
this.pages.show(this.factsView);
}
Apparently you can't show a view on a onRender function... I had to change the way the FactsLayout is initialized:
var layout = new FactsLayout({
slug: slug
});
layout.render();
var factsMenu = new FactsMenu({ slug: slug });
var factsView = new FactsMenuView({model: factsMenu});
layout.pages.show(factsView);
Maybe I did not understand your question well but if you need to listen an event fired from an item view within your composite view you should do like the following.
Within the item view test method.
this.trigger("test");
Within the composite view initialize method.
this.on("itemview:test", function() { });
Note that when an event is fired from an item of a CollectionView (a CompositeView is a CollectionView), it is prepended by itemview prefix.
Hope it helps.
Edit: Reading you question another time, I think this is not the correct answer but, about your question, I guess the click in the composite view is captured by the item view. Could you explain better your goal?
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/