Backbone - aggregator event firing for all models in collection - javascript

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()

Related

Marionette CompositeView undelegating childview events on setElement

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.

Display collection length in a view in Marionette.js

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

Re-render view without models changed backbone

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);

Collection add event listener in Backbone

I am trying to update my view whenever I add a new model to my collection. My first question is do I automatically add a model to my collection when I save that model, like:
PostsApp.Views.Form = Backbone.View.extend({
template: _.template($('#form-template').html()),
render: function(){
this.$el.html(this.template(this.model.toJSON()));
},
events:{
'click button' : 'save'
},
save: function(e){
console.log("is this working");
e.preventDefault();
var newname = this.$('input[name=name-input]').val();
var newadress = this.$('input[name=adress-input]').val();
this.model.save({name: newname, adress : newadress});
}
});
or do I still have to do collection.add()
Other than that to see the new model in my view I am trying to add an 'add' event listener like this:
PostsApp.Views.Posts = Backbone.View.extend({
initialize: function(){
this.collection.on('add', this.addOne, this);
},
render: function(){
this.collection.forEach(this.addOne, this);
},
addOne: function(post){
var postView = new PostsApp.Views.Post({model:post});
postView.render();
this.$el.append(postView.el);
}
});
This not only doesnt work, but when I add the initialize method, it just duplicates everything in my model when the page is first loaded.
Nope.. When you do a model.save , it will just create a zombie model ( If it not already a part of the collection .i.e If a New model is saved) which is not a part of any collection.
So your add event will not be triggered for the collection.
If you want the add event to be triggered , Use the create method of collection , which then will know on which collection the new model has to be added..
collection.create({model});
Then it would internally add the model to the collection and fire the add event
Also it is a better idea to use listenTo instead of attaching events using on
this.listenTo(this.collection, 'add', this.addOne);
Code
PostsApp.Views.Form = Backbone.View.extend({
template: _.template($('#form-template').html()),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
},
events: {
'click button': 'save'
},
save: function (e) {
console.log("is this working");
e.preventDefault();
var newname = this.$('input[name=name-input]').val();
var newadress = this.$('input[name=adress-input]').val();
this.collection.create({
name: newname,
adress: newadress
});
}
});
PostsApp.Views.Posts = Backbone.View.extend({
initialize: function () {
this.listenTo(this.collection, 'add', this.addOne);
},
render: function () {
this.collection.forEach(this.addOne, this);
},
addOne: function (post) {
var postView = new PostsApp.Views.Post({
model: post,
collection : this.collection
});
postView.render();
this.$el.append(postView.el);
}
});

How to pass a model(data) from one view to another in Backbone and edit/delete it?

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.

Categories