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();
}
Related
In Marionette, we have a "master view" that we would like to extend.
var PaginatedDropdown = Marionette.CompositeView.extend({
template: template,
events: {
'click': function () { return 'hello';},
'keyup': function () { return 'goodbye'}
},
childViewOptions: {
tagName: 'li'
}
});
The ideal use case would be extending this view, or class, by more specific views that would implement most of the features, and modify some of the features, of the master class / view:
var MotorcycleColorChooserDropdown = PaginatedDropdown.extend({
events: {
'mouseenter': function () { return 'vroom'; };
}
});
The problem is we're not sure how to selectively modify things such as the events hash, or override certain childview options. Specifically:
If MotorcycleColorChooserDropdown has an events object, it will override all of the events the PaginatedDropdown is listening for. How do we mix and match? (allow having an events object on MotorcycleColorChooserDropdown that combines itself with PaginatedDropdown's events object?
Potentially unsolvable: What if we want all the functionality of the PaginatedDropdown click event, but we also want to add to it a little bit in MotorcycleColorChooserDropdown? Do we just have to manually stick all the functionality from the Parent into the Child class?
We have considered simply not doing extended views like this, or doing things like MotorcycleColorChooserDropdown = PaginatedDropdown.extend(); and then one at a time doing MotorcycleColorChooserDropdown.events = PaginatedDropdown.events.extend({...}); but that seems messy, ugly, and I'm sure there's a better way.
Here is what i've been doing
var PaginatedDropdown = Marionette.CompositeView.extend({
template: template,
events: {
'click': 'onClick',
'keyup': function () { return 'goodbye'}
},
onClick:function(e){
return 'hello'
},
childViewOptions: {
tagName: 'li'
}
});
var MotorcycleColorChooserDropdown = PaginatedDropdown.extend({
events: _.extend({
'click': 'onClick',
'mouseenter': function () { return 'vroom'; };
},PaginatedDropdown.prototype.events),
onClick:function(e){
PaginatedDropdown.prototype.onClick.call(this,e)
//code
}
});
For your first question, i just extend the child events with the parent events.
As for the second, i just call the parent method from the child, passing in the child context and the event object.
It is quite verbose, but also very explicit. Someone reading your code will know exactly what's going on.
You could:
var PaginatedDropdown = Marionette.CompositeView.extend({
template: template,
childViewOptions: {
tagName: 'li'
},
"events": function() {
'click': 'onClick',
'keyup': 'onKeyUp'
},
"onClick": function() {
return 'hello';
},
"onKeyUp": function() {
return 'hello';
},
});
var MotorcycleColorChooserDropdown = PaginatedDropdown.extend({
"events" : function() {
//Question:
//Basically extending the first events by using the marionette event function and extending it.
var parentEvents = PaginatedDropdown.prototype.events,
events = _.extend({}, parentEvents);
events['mouseenter'] = this.onMouseEnter;
//add all of the events of the child
return events;
}
"onMouseEnter": function() {
return 'vroom';
},
"onClick": function() {
//Question 2:
//Applying the parent's method
PaginatedDropdown.prototype.onClick.apply(this, arguments);
//and adding new code here
}
});
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 have the following structure:
var PopupView = Backbone.View.extend({
el:'#shade',
events:{
'click .popup-cancel': 'hide',
'click .popup-commit': 'commit',
},
show: function(){
...
},
hide: function(){
...
},
initialize: function(){
_.bindAll(this, 'show','hide','commit');
}
});
var Popup1 = PopupView.extend({
render: function(){
...
},
commit: function(){
console.log('1');
...
},
});
var Popup2 = PopupView.extend({
render: function(){
...
},
commit: function(){
console.log('2');
...
},
});
The problem is that when I click .popup-commit from one of the popups, it actually triggers the methods of both of them. I've tried moving the declaration of events and initialize() up into the child classes, but that doesn't work.
What's going on, and how can I fix it (so that the commit method of only the view I'm triggering it on gets fired)?
Your problem is right here:
el:'#shade'
in your PopupView definition. That means that every single instance of PopupView or its subclasses (except of course those that provide their own el) will be bound to the same DOM node and they will all be listening to events on on id="shade" element.
You need to give each view its own el. I'd recommend against ever setting el in a view definition like that. I think you'll have a better time if you let each view create (and destroy) its own el. If you do something like:
var PopupView = Backbone.View.extend({
className: 'whatever-css-class-you-need',
tagName: 'div', // or whatever you're using to hold your popups.
attributes: { /* whatever extra attributes you need on your el */ },
//...
});
then your views will each get their own el. See the Backbone.View documentation for more information on these properties.
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?
Yip I am a novice to backbone and underscore. First of all let me say I have read through all the online examples, but I'm still missing something.
Whats happening is I'm loading up my list of objects fine, but when I click delete its going through all the objects. I know this is because I'm not assigning the individual items correctly, but I cannot see what is causing this.
I would love some help.
Here is basic html code
<div id="itemid" class="view">
<span class="text">{{-text}}</span>
<a id="dele" data-role="button" data-inline="true" data-icon='delete' >Delete</a>
</div>
This is my itemlist code
bb.view.List = Backbone.View.extend(_.extend({
tagName: "ul",
initialize: function( items ) {
var self = this
_.bindAll(self)
self.setElement('#itemid')
self.elem = {
text: self.$el.find('#text')
}
self.items = items
},
render: function(items) {
var self = this
self.$el.empty()
self.items.each(function(item){
var itemview = new bb.view.Item({
model: item
})
itemview.render()
})
}
},scrollContent))
Now finally the itemview for individual items, note the template code below.
bb.view.Item = Backbone.View.extend(_.extend({
tagName: "li",
events: {
'tap #dele': function(){
var self = this
self.removed()
return false;
}
},
render: function(){
var self = this
_.bindAll(this)
self.setElement('#itemid')
self.elem = {
dele: self.$el.find('#dele')
}
var html = self.tm.item( self.model.toJSON() )
$(this.el).append( html )
},
removed: function()
{
var self = this
this.model.removed();
}
},{
tm: {
item: _.template( $('#itemid').html() )
}
}))
Hope someone can help
mark
I know it's not directly related to your question, but I have couple suggestions re. your code:
You seem to use var self = this; self.method() everywhere. You do not to do this unless you need to pass this into a closure. But yes, you need it when you go into each iterator.
You do not need to call _.bindAll(self) on all methods. Again, you are better off explicitly binding this to methods, e.g.:
this.collection.bind('reset', this.render, this);
this.el.on('click', 'a', this.handleClick.bind(this));
You probably don't realize and possibly are not even concerned about your client's resources at this point, but binding your environment to this creates a lot of closures (allocating memory) that you will never use.
Now with your issue, I would refactor your code as follows:
var itemTmp = $('#itemid').html();
bb.view.Item = Backbone.View.extend({
tagName: "li",
events: {
'tap #dele': "deleteItem"
},
render: function(){
this.el = _.template(itemTmp)(this.model.toJSON());
},
deleteItem: function() {
this.model.destroy(); // deletes model
this.remove(); // deletes view
}
});
bb.view.List = Backbone.View.extend({
tagName: "ul",
initialize: function(items) {
this.setElement('#itemid');
this.collection = items;
this.render();
},
render: function() {
var self = this; // yes, you will need it in the iterator
this.$el.empty();
this.collection.each(function(model){
var itemview = new bb.view.Item({
model: model
});
itemview.render();
self.$el.append(itemview.el);
});
}
});
_.extend(bb.view.List.prototype, scrollContent);
Note that Item view does not insert its HTML into DOM (or parent view element). Basically it's a better practice when sub-views or dependencies do not access parent's element. Therefore you can either have render() return HTML, or you can access View's instance el attribute from the module that instantiated it.
Last observation -- when you have an app with A LOT of views, you don't want to create new View instance for each list item. You're better off inserting DOM nodes with unique id's and then on DOM events read these ids and parse item ids, and look up items by this.collection.get(id). But again, this is coming from pure performance considerations.