I am trying to add a simple event to the children under my compositeview but it is not triggering at all..and frankly I am not sure why, it seems so simple, I could do this just fine with normal backbone.view.
In the example below, the alert is not triggered at all, however when I purposefully change the function name the event binds to , to something else that doesnt exist, it complaints that the function doesnt exist, so I think it's something else...help?
App.View.ContentContainer = Backbone.Marionette.CollectionView.extend({
className:'content_container',
itemView:App.View.ContentBrowseItem,
events:{
'click .browse_item':'focus_content'
},
initialize:function () {
//this.views = {} //indexed by id
//this.create_modal_container()
var coll = this.collection
coll.calculate_size()
coll.sort_by('title', -1)
},
focus_content:function (e) {
alert('here???')
var $modal_container = this.$modal_container
var content_id = $(e.currentTarget).data('content-id')
var $selected_view = this.views[content_id]
var $focused_content = new App.View.FocusedItem({model:$selected_view.model})
$modal_container.empty().show().append($focused_content.el).reveal().bind('reveal:close', function () {
$focused_content.close()
})
return false
},
onShow:function(){
this.$el.addClass('content_container').isotope({
selector:'.content_item',
resizable:true,
layoutMode:'masonry',
masonry:{ columnWidth:64 }
})
}
EDIT: this is the resulting HTML: http://pastebin.com/uW2X8iPp the div.content_container is the resulting el of App.View.ContentContainer
Is .browse_item a selector for the App.View.ContentBrowseItem itemView element? In that case, you need to bind the event in the ItemView definition, not in the CollectionView definition. The reason is that events are bound when a view is rendered. The CollectionView itself is rendered before any of its child itemViews.
Also, if you are opening up another modal view on this click event, I would let the app handle that, rather than your CollectionView
Try something like this:
App.View.ContentBrowseItem = Backbone.Marionette.ItemView.extend({
...
initialize: function() {
// Maintain context on event handlers
_.bindAll(this, "selectItem")
},
events: {
"click" : "selectItem"
}
selectItem: function() {
App.vent.trigger("item:select", this.model);
}
...
});
And to actually show the modal detail view:
App.vent.on("item:select", function(itemModel) {
var detailView = new App.View.FocusedItem({ model: itemModel });
// You may also want to create a region for your modal container.
// It might simplify some of your `$modal_container.empty().show().append(..).etc().etc()
App.modalRegion.show(detailView);
});
Allowing each of your views to handle their own events is part of what makes Backbone and Marionette so beautiful. You'll just want to avoid one view getting all up in another view's business (eg. a CollectionView trying to handle its ItemView's events, an ItemView creating event bindings to show and close a separate modal view, etc.)
Hope this helps!
Related
I want to perform an action, clearing parent element, after a collection has fetched his models but prior to the models rendering.
I've stumbled upon before and after render methods yet they are model specific, which will cause my parent element to clear before every model rendering.
I'm able of course to perform the action pre-fetching yet I want it to occur when fetch is done and before models are rendered.
I tried using reset and change events listening on the collection yet both resulted unwanted end result.
Reset event seamed to go in that direction yet the passed argument was the entire collection and not a single model from the collection, therefore using the add event callback wasn't possible due to difference in argument type (collection and not a model as required)
Any ideas how to invoke a callback when fetch a collection fetch is successful yet models are yet to be rendered?
The model contains the returned attributes while collection contains url for fetching and parse method to return argument wrapped object.
Below is the code I use to render the collection view, which is basically rendering each model's view within the collection.
Collection View
---------------
var FoosView = Backbone.View.extend({
el: '#plans',
events: {
//'click tr': 'rowClick'
},
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
_.bindAll(this, "render");
this.render();
},
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
...
});
The model view
--------
var FooView = Backbone.View.extend({
tagName: 'li',
initialize: function(options) {
this.options = options || {};
this.tpl = _.template(fooTpl);
},
render: function() {
var data = this.model.toJSON();
this.$el.html(this.tpl(data));
return this;
}
});
Thanks in advance.
OK, I think I understand your question and here is a proposed solution. You are now listening to the reset event on your collection and calling this.renderAll. this.renderAll will take the list of models from the collection and render them to the page, but only AFTER the list element has been emptied. Hope this helps :).
var FoosView = Backbone.View.extend({
el: '#plans',
collection: yourCollection, // A reference to the collection.
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
this.listenTo(this.collection, 'reset', this.renderAll);
},
renderAll: function() {
// Empty your list.
this.$el.empty();
var _views = []; // Create a list for you subviews
// Create your subviews with the models in this.collection.
this.collection.each(function(model) {
_views.push(new FooView({model: model});
});
// Now create a document fragment so as to not reflow the page for every subview.
var container = document.createDocumentFragment();
// Append your subviews to the container.
_.each(_views, function(subview) {
container.appendChild(subview.render().el);
});
// Append the container to your list.
this.$el.append(container);
},
// renderNew will only run on the collections 'add' event.
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
});
I am forced to assume a few things about you html, but I think the above code should be enough to get you up and running. Let me know if it works.
I'm not totally sure about what you are asking but have you tried:
MyCollection.fetch({
success: function(models,response) {
//do stuff here
}
});
Also you may be interested taking a look at http://backbonejs.org/#Model-parse
Hope it helps!
Edit: there is no direct link between fetching and rendering my bet is that you binded rendering to model change.
USE===============>>>> http://backbonejs.org/#Model-parse
I have created a collection passing a collection view and a collection. The collection references a model I have created. when fetching the collection the items get rendered succesfully, but when the models change, the itemViews are not being re-rendered as expected.
for example, the itemAdded function in the tweetCollectionView is called twice ( two models are added on fetch ) but even though the parse function returns different properties over time for those models ( I assume this would call either a change event on the collection, or especially a change event on the model, which I have tried to catch in the ItemView ) the itemChanged is never called, and the itemViews are never re-rendered, which i would expect to be done on catching the itemViews model change events.
The code is as follows below:
function TweetModule(){
String.prototype.parseHashtag = function() {
return this.replace(/[#]+[A-Za-z0-9-_]+/g, function(t) {
var tag = t;
return "<span class='hashtag-highlight'>"+tag+"</span>";
});
};
String.prototype.removeLinks = function() {
var urlexp = new RegExp( '(http|ftp|https)://[\w-]+(\.[\w-]+)+([\w.,#?^=%&:/~+#-]*[\w#?^=%&/~+#-])?' );
return this.replace( urlexp, function(u) {
var url = u;
return "";
});
};
var TweetModel = Backbone.Model.extend({
idAttribute: 'id',
parse: function( model ){
var tweet = {},
info = model.data;
tweet.id = info.status.id;
tweet.text = info.status.text.parseHashtag().removeLinks();
tweet.name = info.name;
tweet.image = info.image_url;
tweet.update_time_full = info.status.created_at;
tweet.update_time = moment( tweet.update_time_full ).fromNow();
return tweet;
}
});
var TweetCollection = Backbone.Collection.extend({
model: TweetModel,
url: function () {
return '/tweets/game/1'
}
});
var TweetView = Backbone.Marionette.ItemView.extend({
template: _.template( require('./templates/tweet-view.html') ),
modelEvents:{
"change":"tweetChanged"
},
tweetChanged: function(){
this.render();
}
})
var TweetCollectionView = Marionette.CompositeView.extend({
template: _.template(require('./templates/module-twitter-feed-view.html')),
itemView: TweetView,
itemViewContainer: '#tweet-feed',
collection: new TweetCollection([], {}),
collectionEvents: {
"add": "itemAdded",
"change": "itemChanged"
},
itemAdded: function(){
console.log('Item Added');
},
itemChanged: function(){
console.log("Changed Item!");
}
});
this.startInterval = function(){
this.fetchCollection();
this.interval = setInterval( this.fetchCollection, 5000 );
}.bind(this);
this.fetchCollection = function(){
this.view.collection.fetch();
this.view.render();
}.bind(this);
//build module here
this.view = new TweetCollectionView();
this.startInterval();
};
I may be making assumptions as to Marionette handles event bubbling, but according to the docs, I have not seen anything that would point to this.
Inside your CollectionView, do
this.collection.trigger ('reset')
after model have been added.
This will trigger onRender () method in ItemView to re-render.
I know I'm answering an old question but since it has a decent number of views I thought I'd answer it correctly. The other answer doesn't address the problem with the code and its solution (triggering a reset) will force all the children to re-render which is neither required nor desired.
The problem with OP's code is that change is not a collection event which is why the itemChanged method is never called. The correct event to listen for is update, which according to the Backbone.js catalog of events is a
...single event triggered after any number of models have been added
or removed from a collection.
The question doesn't state the version of Marionette being used but going back to at least version 2.0.0 CollectionView will intelligently re-render on collection add, remove, and reset events. From CollectionView: Automatic Rendering
When the collection for the view is "reset", the view will call render
on itself and re-render the entire collection.
When a model is added to the collection, the collection view will
render that one model in to the collection of child views.
When a model is removed from a collection (or destroyed / deleted),
the collection view will destroy and remove that model's child view
The behavior is the same in v3.
I have a Google Map where user can click (on the map everywhere), the click event opens a Bootstrap modal window, contained a form. My question is, how/where to handle this submit event to add a marker to the marker collection, save it to the db, etc.
Currently I have a Map View, that renders the google map, and adds an event listener for the click. Clicking on the map opens the Modal.
App.Views.Map = Backbone.View.extend({
...
initializeMap : function(){}
...
addMapEventlistener : function() {
google.maps.event.addListener(this.map, 'dblclick', function(event) {
var coords = event.latLng.toUrlValue();
var carray = coords.split(",");
var model = new Backbone.Model({ coords: carray });
var view = new App.Views.Modal({ model: model });
var $modalEl = $("#modal");
$modalEl.html(view.render().el);
$modalEl.modal();
});
}
App.Views.App = Backbone.View.extend({
initialize: function() {
var addMarkerView = new App.Views.AddMarker({ collection: App.markers });
}
});
// add marker view
App.Views.AddMarker = Backbone.View.extend({
el: '#addForm',
initialize: function() {
$('<input>', {
type: 'submit',
value: 'Submit',
class: 'smt'
}).appendTo(this.$el);
console.log('AddMarker init run'); // this echoed out
},
events: {
'submit' : 'addMarker'
},
addMarker: function(e) {
e.preventDefault();
alert('hello');
},
});
my guess is that the form rendered after the click event on the map, so I have to set backbone event listening somehow after the modal opens, and handle the form submission in a collection view, right?
You may want to use the events object to bind your listeners.
About when (re)binding your events. When the view is instantiated, the listeners specified in the events object will be bound to the view element. This implies that if your event targets a child element, the fact that the child exists or not at that moment doesn't matter. Now, you'll have to rebind your listeners in the special case where you change your view element without the setElement method.
Example:
<div id="#mydiv"></div>
And you want to bind an event on the buttons inside this div (you'll create some afterwards.).
Well, here's an example.
Ok, solved. My bad. I added events to the wrong view, I have to add it to the App.Views.Modal view (of course the event happens in the modal). Thanks for the time!
I have a two views:
1 LeftView (maximized when RightView is minimized & vice versa)
2 RightView (containing)
- collection of
- RightItemView (rendering RightItemModel)
When RightView is maximized and the user clicks a RightItemView, I want to maximize LeftView and display something according to the data from the clicked RightItemView.
What's the proper way to wire them?
I would recommend using the Backbone.Events module:
http://backbonejs.org/#Events
Basically, this line is all it takes to create your event dispatcher:
var dispatcher = _.clone(Backbone.Events);
Then all of your views can trigger/listen for events using the global dispatcher.
So, in RightItemView you would do something like this in the click event:
dispatcher.trigger('rightItemClick', data); // data is whatever you need the LeftView to know
Then, in LeftView's initialize function, you can listen for the event and call your relevant function:
dispatcher.on('rightItemClick', this.maximizeAndDisplayData);
Assuming your LeftView would have a function like so:
maximizeAndDisplayData: function(data) {
// do whatever you need to here
// data is what you passed with the event
}
The solution #jordanj77 mentioned is definitely one of the correct ways to achieve your requirement. Just out of curiosity, I thought of another way to achieve the same effect. Instead of using a separate EventDispatcher to communicate between the two views, why shouldn't we use the underlying model as our EventDispatcher? Let's try to think in those lines.
To start with, add a new boolean attribute to the RightItem model called current and default it to false. Whenever, the user selects the RightItemView, set the model's current attribute to true. This will trigger a change:current event on the model.
var RightItem = Backbone.Model.extend({
defaults: {
current: false,
}
});
var RightItemView = Backbone.View.extend({
events: {
'click li': 'changeCurrent'
}
changeCurrent: function() {
this.model.set('current', true);
}
});
On the other side, the LeftView will be handed a Backbone.Collection of RightItem models during creation time. You would anyways have this instance to supply the RightView isn't it? In its initialize method, the LeftView will listen for change:current event. When the event occurs, LeftView will change the current attribute of the model it is currently displaying to false and start displaying the new model that triggered this event.
var LeftView = Backbone.View.extend({
initialize: function() {
this.collection.on('change:current', this.render, this);
},
render: function(model) {
// Avoid events triggered when resetting model to false
if(model.get('current') === true) {
// Reset the currently displayed model
if (this.model) {
this.model.set('current') = false;
}
// Set the currently selected model to the view
this.model = model;
// Display the view for the current model
}
}
});
var leftView = new LeftView({
// Use the collection that you may have given the RightView anyways
collection: rightItemCollection
});
This way, we get to use the underlying model as the means of communication between the Left and Right Views instead of using an EventDispatcher to broker for us.
The solution given by #Ganeshji inspired me to make a live example
I've created 2 views for this.
var RightView = Backbone.View.extend({
el: $('.right_view'),
template: _.template('<p>Right View</p>'),
renderTemplate: function () {
this.$el.html('');
this.$el.append(this.template());
this.$link = this.$el.append('Item to view').children('#left_view_max');
},
events: {
'click #left_view_max' : 'maxLeftView'
},
maxLeftView: function () {
//triggering the event for the leftView
lView.trigger('displayDataInLeftView', this.$link.attr('title'));
},
initialize: function (options) {
this.renderTemplate();
}
});
var LeftView = Backbone.View.extend({
el: $('.left_view'),
template: _.template('<p>Left View</p>'),
renderTemplate: function () {
this.$el.html('');
this.$el.append(this.template());
},
displayDataInLeftView: function (data) {
this.$el.append('<p>' + data + '</p>');
},
initialize: function (options) {
//set the trigger callback
this.on('displayDataInLeftView', this.displayDataInLeftView, this);
this.renderTemplate();
}
});
var lView = new LeftView();
var rView = new RightView();
Hope this helps.
So I have a View that looks like this.
//base class
var SelectListView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'addOne', 'addAll');
this.collection.bind('reset', this.addAll);
},
addAll: function() {
this.collection.each(this.addOne);
},
events: {
"change": "changedSelected"
},
changedSelected: function() {
this.selected = $(this.el);
this.setSelectedId($(this.el).val());
}
});
//my extended view
var PricingSelectListView = SelectListView.extend({
addOne: function(item) {
$(this.el).append(new PricingView({ model: item }).render().el);
}
});
I have instantiated the view like this...
var products = new ProductPricings();
var pricingsView = new PricingSelectListView({
el: $("#sel-product"),
collection: products
});
Somewhere else (another views custom method)I have updated the pricing view's collection
pricingsView.collection = new ProductPricings(filtered);
This does not seen to do anything.
pricingsView.render();
So now the collection has fewer items but the new view is never rendered or refreshed in the DOM.
How to I do I 1.) refresh the rendering in the DOM? 2.) Make it automatically refresh the DOM? Do I have to somehow tell it to render when ever the collection changes?
You bound addOne() to a reset event. When you just replace the pricingsView.collection instance then that event is not triggered and addOne() is not executed.
Try instead:
pricingsView.collection.reset(filtered);
This might work since you bind to collection's reset event already:
pricingsView.collection.reset(filtered);
http://backbonejs.org/#Collection-reset
You still have tweak your rendering logic to remove old markup from the view when reset happens.