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.
Related
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 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?
OK, I've done some reading on this and I'm pretty sure I know what the problem relates to I Just don't know the best way to fix it. I've got the standard backbone router that sets me up with an item details view, then when I click on a button called "start" it creates a new view which takes me to a sort of a game that people can play with some buttons on the bottom that have "click" events attached. This second view is not called through the router but directly from the first view.
The problem is the second time someones goes back to the homescreen and does it again, this time there are two events attached to each button. The third time there are three events. Obviously the original views are still listening to these buttons. I've read about this and calling the Remove() method but is this what I need to do? If so where do I call remove? Relevant Code below:
1ST VIEW
window.GameDrillView = Backbone.View.extend({
initialize: function () {
this.render();
},
render: function () {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events: {
"click .start" : "startGameDrill",
},
startGameDrill: function () {
var start = $('#start').val();.
var stop = $('#stop').val();.
var StartView = new GameDrillStartView({model: this.model, el: $('#content')[0], start: start, stop:stop});
}
});
START VIEW
window.GameDrillStartView = Backbone.View.extend({
// declare variables
initialize: function () {
this.render();
},
events: {
"click .nextstage" : "nextstage", // 2ND TIME THROUGH GETS CALLED TWICE
},
nextstage: function () {
// Do some stuff //
this.render(); //Re-render
},
render: function () {
// Do some variables stuff
this.$el.html(this.template(jQuery.extend(this.model.toJSON(), extended_options)));..
return this;
}
});
When changing view you need to call undelegateEvents() method from the Backbone.View. It disable listening all the elements events mentioned in events { } block. Also if you need to destroy old view you can call remove() method of the view which will call undelegateEvents() internally.
update (example from official site)
var Workspace = Backbone.Router.extend({
routes: {
"help": "help", // #help
"search/:query": "search", // #search/kiwis
"search/:query/p:page": "search" // #search/kiwis/p7
},
help: function() {
if (this.currentView)
this.currentView.undelegateEvents();
this.currentView = new HelpView();
},
search: function(query, page) {
if (this.currentView)
this.currentView.undelegateEvents();
this.currentView = new SearchView();
}
});
An option is to create only one instance of the view:
if(_.isUndefined(this.StartView))
this.StartView = new GameDrillStartView({model: this.model, el: $('#content')[0], start: start, stop:stop});
else
this.StartView.render();
In the render method of GameDrillStartView add the empty method
this.$el.html(this.template(jQuery.extend(this.model.toJSON(), extended_options)))
In this way you won't add more event listeners but you'll update the page everytime the user presses the button.
You can manage the life cycle of StartView in GameDrillView since it seems like a better place to do so.
Got same trouble. Messy solution:
var current_view = false;
var prev_view = false;
var AppRouter = Backbone.Router.extend({
routes: {
"events/:id": "viewEvent",
}
});
var app_router = new AppRouter;
app_router.on('route:viewEvent', function (event_id) {
var _event = new Event({id:event_id});
current_view = new EventView({
model: _event,
});
});
//Will be called after route:viewEvent
app_router.on('route', function () {
if(prev_view) {
prev_view.undelegateEvents();
}
prev_view = current_view;
});
Not sure, how to make it without having current_view and prev_view out of router scope.
I am writing a backbone.js app, and I have a problem.
My collections do not fire events, can anyone spot the problem in the code bellow? I get the render-feedback, the initializer feedback.. but the append method is never called. I know that the "../app" returns a list with tro json items. And I can even see that these are being created in the collection.
Why do my event not get called?
window.TablesInspectorView = Backbone.View.extend({
tagName: "div",
initialize: function () {
console.log('Initializing window.TablesInspectorView');
// setup the tables
this.data = new Backbone.Collection();
this.data.url = "../app";
this.data.fetch();
// some event binds..
this.data.on("change", this.render , this);
this.data.on("add" , this.append_item, this);
},
render: function(){
console.log("render");
_.each(this.data.models, this.append_item);
},
append_item: function(item) {
console.log("appended");
}
});
According to my knowledge , the backbone fetch() is an asynchronous event and when it completes the reset event is triggered ,
When the models belonging to the collection (this.data) are modified , the change event is triggered, so im guessing you have not got that part correct.
so i would do something like this :
window.TablesInspectorView = Backbone.View.extend({
tagName: "div",
initialize: function () {
console.log('Initializing window.TablesInspectorView');
// setup the tables
this.data = new Backbone.Collection();
this.data.url = "../app";
this.data.fetch();
// some event binds..
this.data.on("reset", this.render , this); //change here
this.data.on("add" , this.append_item, this); // i dont see a this.data.add() in you code so assuming this was never called ?
},
render: function(){
console.log("render");
_.each(this.data.models, this.append_item);
},
append_item: function(item) {
console.log("appended");
}
});
I'm working on my first app using bbjs, after 10 tutorials and endless sources I am trying to come up with my code design.
I ask what is the best practice with views and templates. Also there is an events problem I am struggling with.
As I understand, the view is to be responsible for one element and its contents (and other sub-views).
For the code to be manageable, testable, etc.. the element/template is to be passed to the view on creation.
In my app Imho the view should hold the templates, because the visible element has many "states" and a different template for each state.
When the state changes, I guess its best to create a new view, but, is it possible for the view to update itself with new element?
App.Box = Backbone.Model.extend({
defaults: function() {
return {
media: "http://placehold.it/200x100",
text: "empty...",
type: "type1"
};
}
});
App.BoxView = Backbone.View.extend({
template: {},
templates: {
"type1": template('appboxtype1'),
"type2": template('appboxtype2')
},
events: {
'click .button': 'delete'
},
initialize: function(options) {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
this.render();
},
render: function() {
this.template = this.templates[ this.model.get("type") ];
// first method
this.$el.replaceWith( $($.parseHTML(this.template(this))) );
this.$el.attr("id", this.model.cid);
// second method
var $t_el = this.$el;
this.setElement( $($.parseHTML(this.template(this))) );
this.$el.attr("id", this.model.cid);
$t_el.replaceWith( this.$el );
this.delegateEvents();
//$('#'+this.model.cid).replaceWith( $(g.test.trim()) );
//! on the second render the events are no longer bind, deligateEvents doesn't help
return this;
},
// get values
text: function() { return this.model.get('text'); },
media: function() { return this.model.get('media'); },
delete: function() {
this.model.destroy();
}
});
Thanx! :)
Instead of trying to replace the view's root element ($el), just replace its content.
this.$el.html(this.template(this));
Events should still work then.
try this
render: function() {
html = '<div>your new html</div>';
var el = $(html);
this.$el.replaceWith(el);
this.setElement(el);
return this;
}
$.replaceWith will only replace the element in the DOM. But the this.$el still holds a reference to the now displaced old element. You need to call this.setElement(..) to update the this.$el field. Calling setElement will also undelegateEvents and delegateEvents events for you.
I came up with this solution: http://jsfiddle.net/Antonimo/vrQzF/4/
if anyone has a better idea its always welcome!
basically, in view:
var t_$el = this.$el;
this.$el = $($.parseHTML(this.template(this)));
this.$el.attr("id", this.cid);
if (t_$el.parent().length !== 0) { // if in dom
this.undelegateEvents();
t_$el.each(function(index, el){ // clean up
if( index !== 0 ){ $(el).remove(); }
});
t_$el.first().replaceWith(this.$el);
this.delegateEvents();
}