Backbone Layoutmanager delegateEvents on subview not working - backbone.js

What I have is the following:
Views.Parent = Backbone.View.extend( {
template: "parent",
initialize: function( ) {
this.render( );
},
beforeRender: function( ) {
this.setView( ".foo", new Views.Child( {
model: this.model
} );
},
afterRender: function( ) {
this.$el.html( Handlebars.compile( this.$el.html( ) ).call( this, this.model.attributes ) );
var foo = this.getView( ".foo" );
foo.delegateEvents( );
}
} );
Views.Child = Backbone.View.extend( {
template: "child",
events: {
"click input": "inputClick"
},
initialize: function( ) {
this.listenTo( this.model, "change", this.render );
},
inputClick: function( ) {
console.info( "Input Clicked" );
},
afterRender: function( ) {
this.$el.html( Handlebars.compile( this.$el.html( ) ).call( this, this.model.attributes ) );
var foo = this.getView( ".foo" );
foo.delegateEvents( );
}
} );
The events in Views.Child aren't firing. The view is definitely found and foo.events returns the correct events. I've tried multiple ways of inserting my sub-view, delegateEvents just isn't doing it.
Has anybody else run in to this and can offer any insight?
I should mention I'm using both the events property and this.listenTo in the sub-view. None of them are being fired.
Edit: The parent is being added to another view as such:
this.$el.append( parentView.$el );
Where parentView is an instantiated object of Views.Parent.

When you're calling this.$el.html in afterRender, you are overwriting the view current HTML (and every bound events/child view). As so, you're removing the child view you set in beforeRender.
That's why you got no event firing up.
I'd like to provide more insight as to how you should organize your code... But I can't understand what you're trying to do in afterRender, this make no sense at all. Compile all your template code inside the fetch and render configurations options, not in afterRender.
A quick note:
beforeRender: where you set child views
afterRender: where you bind events or startup jQuery plugins, etc. You should not be touching the DOM here.

Related

Backbone.js model trigger change only if model.on called again

It's my first post here so please be nice ;) I'm trying to create a Backbone+requireJS sample app and I have stumbled upon a strange problem. In my views initialize function I bind to models change event by
this.model.on("change", this.change());
Event is triggered correctly when data is loaded and all is fine. In my view I also bind to a click event. On click I try to change one of the properties and I was hoping for a change event, but it never came.
I was trying some stuff recommended here on stackoverflow (here, here, and there) but with no luck.
In my guts I feel it has to do with event binding. When I tried to bind again to models on change event inside click callback it started to work. So now I sit here and I'm, hm, confused. Could someone shed some light on this issue?
My View:
define(
[
'jquery',
'underscore',
'backbone',
'text!templates/news/newsListItem.html'
],
function($, _, Backbone, newsListItemTemplate)
{
var NewsListItemViewModel = Backbone.View.extend({
tagName: 'li',
events: {
"click a" : "onLinkClick"
},
initialize: function(){
this.model.on("change", this.changed());
},
render: function()
{
this.$el.html(_.template(newsListItemTemplate, this.model.toJSON()));
},
changed: function()
{
this.render();
console.log("changed");
},
//GUI functions
onLinkClick: function(e)
{
console.log("click!");
this.model.toggle();
this.model.on("change", this.changed());
}
});
var init = function(config){
return new NewsListItemViewModel(config);
}
return {
init : init
}
}
);
My Model:
define(
['jquery', 'underscore', 'backbone'],
function($, _, Backbone){
var NewsListItemModel = Backbone.Model.extend({
toggle: function() {
this.set('done', !this.get('done'));
this.trigger('change', this);
}
});
var init = function(data)
{
return new NewsListItemModel(data);
}
return {
init: init,
getClass: NewsListItemModel
};
}
);
Thanks for any input :)
First, you should use a function as event handler - not its result.
Hence, change the line into this:
this.model.on("change", this.changed.bind(this));
As it stands now, you actually fire this.changed() function just once - and assign its result (which is undefined, as the function doesn't have return statement) to be the model's change event handler.
Second, you shouldn't rebind this handler in onLinkClick: once bound, it'll stay here. It looks like it's more appropriate to trigger this event instead:
onLinkClick: function(e)
{
console.log("click!");
this.$el.toggle();
this.model.trigger('change');
}

Event handling between views

Ok I have a layout like the one in this pic:
The table in the upper part of the screen is made by:
MessageListView
define(['backbone','collections/messages','views/message'], function(Backbone, MessageCollection, MessageView) {
var MessageListView = Backbone.View.extend({
el: '#messagesContainer',
initialize: function() {
this.collection = new MessageCollection();
this.collection.fetch({reset:true});
this.listenTo( this.collection, 'reset', this.render );
this.table = this.$el.find("table tbody");
this.render();
},
render: function() {
this.collection.each( function(message, index) {
this.renderMessage(message, index);
}, this);
},
renderMessage: function(message, index) {
var view = new MessageView({
model:message,
className: (index % 2 == 0) ? "even" : "odd"
});
this.table.append( view.render().el );
}
});
return MessageListView;
});
MessageView
define(['backbone','models/message'], function(Backbone, MessageCollection, MessageView) {
var MessageView = Backbone.View.extend({
template: _.template( $("#messageTemplate").html() ),
render: function() {
this.setElement( this.template(this.model.toJSON()) );
return this;
},
events:{
'click':'select'
},
select: function() {
// WHAT TO DO HERE?
}
});
return MessageView;
});
AppView
define(['backbone','views/messages'], function(Backbone, MessageList) {
var App = Backbone.View.extend({
initialize: function() {
new MessageList();
}
});
return App;
});
I will soon add a new view (maybe "PreviewView") in the lower part of the screen.
I want to make something happen inside the "PreviewView" when user clicks a row.
For example, it could be interesting to display other model's attributes (details, e.g.) inside the PreviewView.
What is the best practice?
holding a reference to PreviewView inside each MessageView ?
triggering events inside select method, and listening to them using on() inside the preview view.
using a transient "selected" attribute in my model, and make PreviewView listen to collection "change" events?
Thank you, if you need more details tell me please, I'll edit the question.
Not sure about the best practice but I found this solution trivial to implement. I created a global messaging object, bus, whatever:
window.App = {};
window.App.vent = _.extend({}, Backbone.Events);
You have to register the "triggerable" functions of PreviewView on the previously created event bus (according to your example, this should be in the PreviewView):
initialize: function () {
App.vent.on('PreviewView.show', this.show, this);
}
Now you should be able to trigger any of registered events from anywhere within your application by calling: App.vent.trigger. For example when the user click on a row you will have something similar:
App.vent.trigger('PreviewView.show');
in case if you have to send and object along with the triggered event use:
App.vent.trigger('PreviewView.show', data);

In a Backbone View's delegated event, how to access the source element of the event?

I am in the process of rewriting jQuery code to Backbone, but am stuck on a seemingly trivial issue:
Inside an event callback on an element that's a child of the main View element, how do I access the specific child element that was clicked (for example)?
var Composer = Backbone.View.extend({
events: {
"click #postnow": "postnow"
},
postnow: function(){
// #fixme:
var btn = $("#postnow");
// Do something
}
});
new Composer({el: $('.composer')});
In jQuery, I would use $(this), but in Backbone it refers to the View element, not the clicked child.
Is there any way to do that without explicitly specifying the selector again?
In view callbacks, you have access to the jQuery events and their properties, more specifically to event.currentTarget
Try
var Composer = Backbone.View.extend({
events: {
"click #postnow": "postnow"
},
postnow: function(e){
var btn = $(e.currentTarget);
}
});
A implicit parameter is sent to the function callback this parameter is a jQuery Event Object and it contains a lot of information including the DOM Element where the event was targeted:
var Composer = Backbone.View.extend({
events: {
"click #postnow": "postnow"
},
postnow: function( event ){
console.log( "postnow()" );
console.log( "this", this );
console.log( "event", event );
console.log( "target", event.target );
console.log( "data-value", $(event.target).attr( "data-value" ) );
}
});
Check the working jsFiddle

Can't get template to display using backbone.js and underscore

I'm really new to Backbone, and I've looked everywhere to try and figure this out. Fundamentally I get how Models, Views, Collections and Templates work together, but for some reason I just can't get my Collection to render in a template. Using Firebug, I get a "this.model is undefined"
Here's my code:
(function($) {
window.Category = Backbone.Model.extend({});
window.Categories = Backbone.Collection.extend({
url: "src/api/index.php/categories?accounts_id=1",
model: Category
});
window.categories = new Categories();
categories.fetch();
window.CategoriesView = Backbone.View.extend({
initialize:function () {
_.bindAll(this,"render");
this.model.bind("reset", this.render);
},
template:_.template($('#tpl-category').html()),
render:function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
var testTargetvar = new CategoriesView({ el: $("#collection") });
})(jQuery) ;
This is the data that my REST service generates:
[
{
"name": "Web Design",
"id": 0
},
{
"name": "Web Development",
"id": 0
},
{
"name": "Waste Management Solutions",
"id": 0
}
]
The "fetch" that I'm using does show the fetched data in Firebug.
And lastly, here's my template:
<div id="collection"></div>
<!-- Templates -->
<script type="text/template" id="tpl-category">
<span><%=name%></span>
</script>
I've verified that all the necessary scripts, jquery, backbone, underscore, etc. are being loaded onto the page properly.
I don't see where you're setting the model property of the view object. Your view constructor also seems to be confused between whether it represents the collection of categories (as suggested by the #collection selector) or a single category (as suggested by the template and use of the model property). Here is a working example that should help guide you to your complete solution:
http://jsfiddle.net/RAF9S/1/
( function ( $ ) {
var Category = Backbone.Model.extend( {} );
var Categories = Backbone.Collection.extend( {
url : "src/api/index.php/categories?accounts_id=1",
model : Category
} );
var categories = new Categories( [
{ name : "One" },
{ name : "Two" },
{ name : "Three" }
] );
var CategoryView = Backbone.View.extend( {
initialize : function () {
_.bindAll( this,"render" );
this.model.bind( "reset", this.render );
},
template : _.template( $( '#tpl-category' ).html() ),
render : function ( eventName ) {
this.$el.empty().append(
$( this.template( this.model.toJSON() ) ).html()
);
if ( ! this.el.parentNode ) {
$( "#collection" ).append( this.el );
}
return this;
}
// render
} );
var view, views = {};
categories.each( function ( category ) {
view = new CategoryView( {
tagName : 'span',
model : category
} );
view.render();
views[ category.id ] = view;
} );
} )( jQuery );
In this case I've simply manually populated the collection with several models in lieu of your fetch method. As you can see, I instantiate a view for each model and pass the model object as the model property.
In the render method I empty the view element (this.el / this.$el), then render the template and append the content of its root element to this.$el. That's so I get the new content without wiping out the element itself. Then, for the purposes of this example, I test whether the element is already in the document, and if not I append it to #container.
I always load my templates in my functions...that might help..
initialize: function(){
_.bindAll(this, 'render');
this.template = _.template($('#frame-template').html());
this.collection.bind('reset', this.render);
},
render: function(){
var $stages,
collection = this.collection;
this.template = _.template($('#frame-template').html());

How to connect changes some field in model to reflect on view?

How to connect changes some field in model to reflect on view ?
I have model which holds font-weight and I have that model in view, but how to connect changes of font-weight filed in model to reflect on el from view ?
There are several approachs that can be applied here on depending that how much delicate you want to be.
1. re-render the whole View any time the Model change
initialize: function(){
this.model.on( "change", this.render, this );
}
2. be more precise and only re-render what is needed
initialize: function(){
this.model.on( "change:title", this.renderTitle, this );
this.model.on( "change:body", this.renderBody, this );
this.model.on( "change:fontWeight", this.renderFontWeight, this );
}
This needs the company of minimal render methods that modify the DOM as a surgeon:
renderTitle: function(){
this.$el.find( "h1" ).html( this.model.get( "title" ) );
},
renderBody: function(){
this.$el.find( "p" ).html( this.model.get( "body" ) );
},
renderFontWeight: function(){
this.$el.find( "p" ).css( "font-weight", this.model.get( "fontWeight" ) );
}
3. use subviews for every part of the Model
I'm not offering any example for this approach because the implementation can be more complex. Just think that your actual View is instantiating several SubViews, one for the title, other for the body, and so on. Each one with its own render and binding the changes in its concrete Model attribute and re-render when the attribute changes.
You can check working jsFiddle code for the approachs 1. and 2.
try this:
var Font = {};
Font.Model = Backbone.Model.extend({
defaults: {
font_family: 'Arial, Helvetica, sans-serif',
font_size: 12,
font_weight: 'normal'
}
});
Font.View = Backbone.View.extend({
initialize: function() {
var this_view = this;
this.model.bind('change:font_weight', function(model) {
// Do something with this_view.el
alert('handle the font-weight change');
});
}
});
var myFontModel = new Font.Model();
var myFontView = new Font.View({
model: myFontModel
});
myFontModel.set({font_weight: 'bold'});

Resources