I have a marionette compositeview which I am using to create a item list for a profile page on an app. For the child view, I extend from an already existing ItemView.
When I use this.setElement(this.el.innerHTML) in the compositeview onRender function, all the events set in the child view no longer are triggered and even more so, triggering them in the console on the inspector tool in the browser, does nothing.
However when I do not use setElement, the container div is added to my markup, but now all the events in the child view work.
Can someone help me understand this please.
The Collection I am using has a custom clone method.
I am using a global collection which is updated and stored in cache on each fetch.
When I actually instantiate my view, the collection has already been used and a region in the main layout view has been populated with a item list similar to the one I want to render.
This is how I instantiate my view:
var currentUser = Profile.get('username');
// Perform changes to global collection
Items.url = API + '/items/search?q=' + currentUser + '&size=20';
Items.parse = function (response) {
if (!response.results) {
return response;
} else {
return response.results;
}
};
Items.fetch(
{success: function (collection, response, options) {
this.listOfItems = new View.itemListProfilePage({
template: TemplIds.profilePagePostedItems,
parentClass: 'profile-cols',
collection: Items, // global collection
filterAttr: {user: currentUser},
isFiltered: true,
lazyLoad: true,
childViewContainer: '#profile-items',
childView: View.itemProfilePage.extend({
template: TemplIds.item
})
});
Backbone.trigger('main:show', this.listOfItems); //'main:show' is an event in layoutview which calls region.show
},
remove: false
});
My compositeview:
View.itemListProfilePage = Marionette.CompositeView.extend({
collection: null, //original collection cloned later for filtering
fetch: null, //promise for fetched items
lazyView: null,
options: {
parentClass: '',
filterAttr: {},
isFiltered: false,
lazyLoad: false
},
initialize: function () {
this.stopListening(this.collection);
//Change collection property and re-apply events
this.collection = this.collection.clone(this.options.filterAttr, this.options.isFiltered);
this._initialEvents();
this.collection.reset(this.collection.where(this.options.filterAttr), {reset: true});
this.listenTo(Backbone, 'edit:profileItems', this.addEditClassToSection);
},
onRender: function () {
this.setElement(this.el.innerHTML, true);
},
onShow: function () {
if (this.options.parentClass) {
this.el.parentElement.className = this.options.parentClass;
}
},
addEditClassToSection: function (options) {
if ( options.innerHTML !== 'edit' ) {
this.el.classList.add('edit-mode');
} else {
this.el.classList.remove('edit-mode');
}
},
}
The parent ItemView:
View.Item = Marionette.ItemView.extend({
model: null,
numLikes: null, //live set of DOM elements containing like counter
modalItem: null, //view class with further details about the item to be used within a modal
events: {
'click img.highlight': 'showModal'
},
initialize: function (options) {
var itemWithHeader; //extended item view class with header at the top and no footer
var addToCart;
//Set up all like-related events
this.listenTo(this.model, "change:numLikes", this.updateNumLikes);
this.listenTo(this.model, "change:liked", this.updateLiked);
//Set up the view classes to be used within the modal on click
itemWithHeader = View.ItemWithHeader.extend({
template: this.template,
model: this.model //TODO: move to inside itemDetails
});
itemAddToCart = View.ItemAddToCart.extend({
template: TemplIds.itemAddCart,
model: this.model //TODO: move to inside itemDetails
});
this.modalItem = View.ItemDetails.extend({
template: TemplIds.itemDetails,
model: this.model,
withHeader: itemWithHeader,
addToCart: itemAddToCart
});
},
onRender: function () {
var imgContainerEl;
var likeButtonEl;
//Get rid of the opinionated div
this.setElement(this.el.innerHTML);
this.numLikes = this.el.getElementsByClassName('num');
//Add the like button to the image
likeButtonEl = new View.LikeButton({
template: TemplIds.likeButton,
model: this.model
}).render().el;
this.el.firstElementChild.appendChild(likeButtonEl); //insert button inside img element
},
showModal: function (evt) {
var modalView = new View.Modal({
views: {
'first': {view: this.modalItem}
}
});
Backbone.trigger('modal:show', modalView);
},
});
The itemView for each individual item in my list:
View.itemProfilePage = View.Item.extend({
events: _.extend({},View.Item.prototype.events, {
'click .delete-me': 'destroyView'
}
),
onRender: function () {
View.Item.prototype.onRender.call(this);
this.deleteButtonEl = new View.itemDeleteButton({
template: TemplIds.deleteButton
}).render().el;
this.el.firstElementChild.appendChild(this.deleteButtonEl);
},
destroyView: function (evt) {
this.model.destroy();
}
});
The short answer is that you should not be using setElement.
Backbone specifically uses the extra container div to scope/bind the view's events. When you use setElement you are changing what the parent element is. Since you are doing this in the onRender function, which is called after the template has been rendered and the events have already been bound, you are losing your event bindings.
The correct thing to do if you are going to use Marionette and Backbone is to expect and utilize the "extra" div wrapper that is generated when you render a view. You can take control of the markup for that "wrapper" div by using className, id, and tagName view properties on your view classes.
Related
I'm learning Marionette.js and have a scenario, where my app has:
app.addRegions({
main: '#omen',
newItem: '#addnewitem',
counter: '#counter'
});
These regions. I have these Model/Collections:
var Item = Backbone.Model.extend(),
Items = Backbone.Collection.extend({
model: Item,
url: 'api/items'
}),
I have an Item view and Items view:
ItemView = Mn.ItemView.extend({
tagName: 'tr',
template: '#itemView',
events: {
'click #btnDeleteBook' : 'deleteItem'
},
deleteItem: function() {
app.trigger('item:delete', this.model);
}
}),
ItemsView = Mn.CollectionView.extend({
tagName: 'table',
childView: ItemView,
onShow: function(view) {
TweenMax.staggerFrom($(view).find('td'), 1, {
x: 100
}, 2);
}
}),
I have an initializer function, that listens for events above and does stuff through app.ItemController. It all works fine.
But now I want to add a region (counter region), that displays the total number of items in my collection. I need this to be a separate view ideally, because I will be displaying it in different places.
So I do this:
DisplayCounter = Mn.ItemView.extend({
template: _.template('Total: '+ app.Items.length),
}),
app.Items is an instance of Collection declared above. But even before instantiation of DisplayCounter, I get error:
Uncaught TypeError: Cannot read property 'length' of undefined.
Please help... :(
------------------------- E D I T ----------------------
I've achieved it, but it seems to be so complicated to do such a tiny thing.
Changed my collection like so:
Items = Backbone.Collection.extend({
model: Item,
url: 'api/items',
initialize: function() {
this.listenTo(this, 'add', function() {
app.trigger('collection:updated', this);
});
}
}),
and changed my DisplayCounter like this:
DisplayCounter = Mn.ItemView.extend({
template: _.template('Total: <%=length%>'),
templateHelpers: function() {
return {
length: this.lengthVariable || 0
}
},
initialize: function() {
app.on('collection:updated', function(params){
this.lengthVariable = params.length;
this.render();
}.bind(this));
}
}),
I can't believe there's no easier way to do this.. :/
The code that sets up DisplayCounter is being run before the code that puts an instance of Items into app.Items.
Even if you avoided this problem by assigning app.Items first, you'd still have a problem - the template property is only set once so you'd only ever see the length of app.Items at the time that you define DisplayCounter.
Rather than hard-coding the value directly into the template string, you should supply a value at render time. Mn.View.serializeData allows you to customise the data that is passed into the template function at render time:
DisplayCounter = Mn.ItemView.extend({
template: _.template('Total:: <%= itemCount %>),
serializeData: function() {
return { itemCount: app.Items.length }
}
}),
app.Items is not being defined.
In Marionette you can define which collection or model are your views going to use.
ItemsView = Mn.CollectionView.extend({
tagName: 'table',
childView: ItemView,
collection: myItems // An instance of your collection
onShow: function(view) {
TweenMax.staggerFrom($(view).find('td'), 1, {
x: 100
}, 2);
}
}),
So marionette is going to render one itemView per element in your collection. Then inside of your collection view this.collection is going to refer to the collection instance. So this.collection.length will have what you need.
And in your ItemView you can get the corresponding model by using this.model
I'm trying to use the event aggregator to fire a method off of a model's view. The problem is, when I fire the update or save method for the ItemView, it iterates through all models in the collection. How do I get it to not only fire properly for the model which the view represents (or a new modal in the save method's case), but also prevent it from firing for every model in the collection?
This application consists of a collection of Items, each Item has a model which is rendered into an ItemView and listed on the page. If a user clicks the edit item icon, then a ModalView is instantiated and the current Item model data is injected into the ModalView.
The ModalView which gets loaded with a template for the respective task. For this instance, i'm loading a template to edit the Item. Here's a summary of the relevant code:
var ModalView = Backbone.View.extend({
tagName: "section",
className: "modal",
events: {
'click .close': 'close',
'click .minimize': 'minimize',
'click .maximize': 'maximize',
'click .save-item': 'saveItem',
},
html: null,
initialize: function(options) {
this.template = _.template(ModalTemplate);
this.vent = options.vent;
},
saveItem: function() {
this.vent.trigger('item.save');
},
});
The item collection's view is here:
var ItemsView = Backbone.View.extend({
tagName: 'ul',
className: 'item-items',
render: function(){
var self = this;
// Make it flex GRRRR
this.$el.addClass('flex-item');
this.collection.each(function(item) {
var date = item.get('created_at');
var itemView = new ItemView({ model: item, vent: App.vent });
this.$el.append(itemView.render().el);
}, this);
return this;
}
});
Finally, the item model's view which contains the edit method that fires the ModalView
var ItemView = Backbone.View.extend({
tagName: 'li',
className: 'item',
events: {
'click .edit-item': 'edit'
},
initialize: function(options) {
this.template = _.template(ItemTemplate);
options.vent.bind("item.save", this.save);
options.vent.bind("item.update", this.update);
},
save: function() {
var attributes, item;
item = new App.api.item.model();
attributes = getMeta(true);
item.save(attributes)
.done(function(res) {
Ui.modal.destroy();
// Re-render items
App.routers.main.render.User.sidebar();
App.routers.main.render.Item.items(function() {
Ui.resizeContent();
});
})
.fail(function(res) {
console.log(res);
});
},
update: function() {
console.log('update') // fires App.api.item.collection.length times
var attributes, item;
item = App.api.item.collection.get(App.rendered.modal.$el.data('id'));
attributes = getMeta();
item.save(attributes)
.done(function(res) {
Ui.modal.destroy();
// Re-render items
App.routers.main.render.Item.items(function() {
Ui.resizeContent();
});
})
.fail(function(res) {
console.log(res);
});
},
edit: function() {
Ui.modal.new(ItemModalTemplate, this.model.attributes);
App.rendered.modal.$el.attr('data-id', this.model.get('_id'));
// New Editor
var editor = document.querySelector('#item-editor');
window.editor = new MediumEditor(editor, editorOptions);
}
});
Obviously I'm missing something fundamental here because console.log('update') in the save method of the ItemView fires for every item in the collection. What I was trying to do was keep the logic for save, and update in the view for the Item for organizational purposes.
Many thanks.
Instead of options hold the model itself in the ItemModelView so you can call save directly without need for events.
Replace this Ui.modal.new(ItemModalTemplate, this.model.attributes); with UI.modal.new(ItemModalTemplate, this.model), and this this.vent.trigger('item.save'); with this.model.save()
I'm having difficulty getting a model with an auto-incrementing "order" attribute working in BackboneJS.
For some reason every order gets set to 1. The length of the collection in the nextOrder function is always 0.
Options = _.extend(Options, {
Models: {
Profile: Backbone.Model.extend({
defaults: function() {
console.log("Defaults");
return {
title: "New Profile",
order: Profiles.nextOrder(),
active: false
};
},
url: "/youdontcare"
})
});
Options = _.extend(Options, {
Collections: {
ProfileList: Backbone.Collection.extend({
model: Options.Models.Profile,
comparator: function(profile) {
console.log("Comparator");
return profile.get('order');
},
nextOrder: function() {
console.log("nextOrder...");
console.log(this.length);
if (!this.length) return 1;
return this.last().get('order') + 1;
},
url: "/youdontcare"
})
});
Options = _.extend(Options, {
Views: {
ProfileView: Backbone.View.extend({
tagName: "li",
template: _.template($('#profile-template').html()),
render: function() {
console.log("Render Profile");
this.$el.html(this.template(this.model.toJSON()));
return this;
}
}),
ProfileListView: Backbone.View.extend({
el: $("#auth_env_list"),
initialize: function() {
Profiles = new Options.Collections.ProfileList;
console.log("INIT LIST");
this.listenTo(Profiles, 'add', this.addOne);
this.listenTo(Profiles, 'reset', this.addAll);
this.listenTo(Profiles, 'all', this.render);
// Suppresses 'add' events with {reset: true} and prevents the app view
// from being re-rendered for every model. Only renders when the 'reset'
// event is triggered at the end of the fetch.
console.log("Fetching");
Profiles.fetch({ reset: true });
console.log("Done fetching");
},
addOne: function (profile) {
console.log("addOne");
var view = new Options.Views.ProfileView({ model: profile });
this.$el.append(view.render().el);
},
addAll: function () {
console.log("addAll");
this.$el.html('');
Profiles.each(this.addOne, this);
},
render: function() {
console.log("RENDER PROFILE LIST VIEW");
if (Profiles.length)
{
}
}
})
});
I can see that the nextOrder function inside the Profiles instance of the Options.Collections.ProfileList collection is called the appropriate number of times for each element that is fetched for the collection... however the length of the collection it tries to compute with this.length always returns 0!
Console output with 5 "Profile" elements:
INIT LIST
Fetching
RENDER PROFILE LIST VIEW
Done fetching
Defaults
nextOrder...
0
Defaults
nextOrder...
0
Defaults
nextOrder...
0
Defaults
nextOrder...
0
Defaults
nextOrder...
0
Comparator
addAll
addOne
Render Profile
addOne
Render Profile
addOne
Render Profile
addOne
Render Profile
addOne
Render Profile
RENDER PROFILE LIST VIEW
RENDER PROFILE LIST VIEW
Is there a better way I could assign an auto incrementing client side ID to these? The only reason I want to do it is to display them in a numbered list.
By calling the collection via the model, you're kind of creating a circular reference, and it's not very efficient if you were going to reuse the code on a different collection. It's possible that you're getting 0 back because it's not referring to the actual collection instance. A better way to accomplish what you want would be to have the collection assign the order number whenever a new model is added to the collection:
// 1st, get rid of the adding an order in your model
// in your collection, add something like the following
initialize: function() {
this.on('add', this.addOrderID);
this.on('reset', this.addOrderIDs);
},
addOrderID: function(model) {
var order = this.length;
model.set({'order': order});
},
addOrderIDs: function() {
var order = this.length;
this.models.forEach( function(model) {
model.set({'order': order});
order++;
}, this);
}
I think that should accomplish what you're looking for.
i'm rendering a view from a collection of user.When a specific attribute(Status=online,offline) in a user change the view correctly show on the dom the value of attribute changed. But if i want render the view without the model in which the attribute is changed or viceversa add to view a model in wich attribute is changed?
This is a code that send to view a collection with users status online:
var user_on=Models.utenti.filter(function(model){
return model.get('status') === "on";
});
var users_online = new Usercollection(user_on);
var page=new Homelistuser({model:users_online});
this.changePage(page);
And this is a view:
var Homelistuser = Backbone.View.extend({
tagName: "ul",
id: "list",
template: Handlebars.compile(template),
initialize: function () {
Models=this.model;
this.model.bind("reset", this.render, this);
$(window).on('orientationchange', this.onOrientationChange);
},
render: function (eventName) {
$(this.el).empty();
_.each(this.model.models, function (ad) {
$(this.el).append(new SingleUserView({
model: ad
}).render().el);
}, this);
return this;
},
You could filter online users in the render function of your view, I believe you called the users_online collection as model in it, so:
model.reset(model.filter(function(model){
return model.get('status') === 'on';
}));
or maybe just filter elements out as you append SingleUserViews
_.each(this.model.models, function (ad) {
if (ad.get('status') !== 'on') return;
$(this.el).append(new SingleUserView({
model: ad
}).render().el);
}, this);
I have a web application using BackboneJS. In this application, I have a LayoutView.js file in which there is a Backbone View (called LayoutView). LayoutView has other functions (methods) that call other views. I am fetching some data in the initialize function of LayoutView, and I need to get this same data (model) in another view and work (update/delete) on it. Below is how I am passing data from LayoutView to myView:
var LayoutView = Backbone.View.extend({
el: $("#mi-body"),
initialize: function () {
var that = this;
this.ConfigData = new Configurations(); //Configurations is a collection
this.ConfigData.fetch({
success: function () {
alert("success");
},
error: function () {
alert("error");
}
});
this.render();
Session.on('change:auth', function (session) {
var self = that;
that.render();
});
},
render: function () {
// other code
},
events: {
'click #logout': 'logout',
'click #divheadernav .nav li a': 'highlightSelected'
},
myView: function () {
if (Session.get('auth')) {
this.$el.find('#mi-content').html('');
this.options.navigate('Myview');
return new MyLayout(this.ConfigData);
}
}
});
Still, I do not know how to "get"/access this data as my current data/model/collection (I am not sure which term is correct) in myView and work on it using Backbone's "model.save(), model.destroy()" methods. Also, whenever an edit/delete happens, the data of ConfigData should be modified and the update should reflect in the html displayed to the user.
Below is the code from MyView:
var MyView = Backbone.View.extend({
tagName: 'div',
id: "divConfigurationLayout",
initialize: function (attrs) {
this.render();
},
render: function () {
var that = this;
},
events: {
"click #Update": "update",
"click #delete": "delete"
},
update: function(){
//code for updating the data like model.save...
},
delete: function(){
//code for deleting the data like model.destroy...
}
});
Now the data I passed is in attrs in the initialize function. How to get this done..?
The syntax for instantiating a Backbone view is new View(options) where options is an Object with key-value pairs.
To pass a collection to your view, you'd instantiate it like so:
new MyLayout({
collection : this.configData
});
Within your view, this.collection would refer to your configData collection.