I created a view and has the ff codes:
var app = app || {};
app.singleFlowerView = Backbone.View.extend({
tagName: 'article',
className: 'flowerListItem',
// tells where to apply the views
template: _.template( $("#flowerElement").html() ),
// render
render: function(){
var flowerTemplate = this.template(this.model.toJSON());
// el contains all the prop above and pass to backbone
this.$el.html(flowerTemplate);
return this;
},
events: {
'mouseover': 'addBgColor',
'mouseout': 'removeBgColor'
},
addBgColor: function(){
this.$el.addBgColor('bgColorImage');
},
removeBgColor: function(){
this.$el.removeBgColor('bgColorImage');
}
});
When I run this to my HTML file I got the error addBgColor and removeBgColor is not a function. I have the CSS for this and all the models and views were set up.
Am I missing something here? Any idea why events doesn't work?
this.$el.addBgColor is the problem.
The events are triggering but you're calling addBgColor on the $el jQuery object, which is not a jQuery function, like the error message is telling you.
Check what's the difference between $el and el.
Tony, your events are cool and they are running they're just not doing anything.
this.addBgColor() will call your function in a view.
this.$el is referring to the html and there's no property called addBgColor assigned to $el.
You need to do something like change the class on your tag with the functions like so...
addBgColor: function(){
this.$el.className = 'bgColorImage'
},
.bgColorImage {
background-image: url('bgColorImage.jpg');
}
I am using backbone.js and I have an event attached to a button that displays a form attached to the selected element. After the new form is submitted, it replaces text on the DOM.
The problem is that the next click on the original button does not launch the menu. I'm not sure what's going on.
events: {
'click .editUser': 'edit',
// 'click #editPerson': 'editPerson',
'click .deleteUser': 'remove'
},
edit: function () {
menuBar = $("#editPerson"); ///this is my new input form
console.log(this.model);
var model = (this.model);
console.log(model);
$(this.el).css("background-color", function (index){
return "rgba(54,42,64, .9)";
});
$(".contact-container").css("margin", function (index){
return "0 5px 100px 0 ";
})
$(this.el).append(menuBar);
console.log($(this.el));
menuBar.show();
$("#editPerson").on('submit', function() {
console.log("hoook");
var name = $(".edit1").val();
var address = $(".edit2").val();
var telephone = $(".edit3").val();
var email = $(".edit4").val();
model.set({name: name});
model.set({address: address});
model.set({tel: telephone});
model.set({email: email});
// $(":button").removeAttr("disabled");
return false;
});
},
Why are you appending the #editPerson to $el if it's already in the DOM? What that does is take the node from the DOM and append it inside your $el. That has a bad smell! The next time your view looks for this node, it won't find it if you have removed the previous view, as it's no longer in the DOM.
If #editPerson is just a form's template, you can retrieve the HTML and append it to your view:
this.$el.append($("#editPerson").html())
(this.$el is equivalent to $(this.el) and is 'faster' because the object already exists, when you create your Backbone View, Backbone will assign a variable this.$el = $(this.el) for convenience)
Then, you can add the 'submit #editPerson': 'onSubmit' key in your events hash and copy your handler code to a new onSubmit function within the View. No need to do the binding manually because Backbone will 'notice' (that's not really how it works, Backbone isn't watching the DOM) that a new element has been added to your view and the event will just work.
I want to query a element in a durandaljs widget, when it's ready.
If i use the selector directly in the data-binding, the element will not be found:
html (no attached view):
<button id="myButton"></button>
<div data-bind="widget: { kind: 'myWidget', options: { btn: $('#myButton') } }"></div>
controller.js:
define(function (require) {
var ctor = function (element, settings) {
var btn = settings.options.btn;
// btn = $('#myButton'); // this will work, but i'm not sure if the DOM is
// currently ready in the constructor
btn.on("click", function () {
console.log("I want to be fired");
});
};
return ctor;
});
Whats the best way to query a DOM element from a durandal widget at start?
I'm not sure where the html fragment belongs to so there are two slightly different answers.
First I'd suggest that you don't pass in the btnas jQuery object ({btn: $('myButton')}) , when you're not sure that it already exists. It's probably better to pass in a selector {btn: '#myButton'} and let the widget figure out how to deal with it.
Does your widget have its own view.html and the button is defined inside? If that's the case than you should take a look at the viewAttached callback.
var ctor = function (element, settings) {
this.btn = settings.options.btn;
};
ctor.prototype.viewAttached = function (view){
var btn = $(this.btn, view);
if ( btn.length > 0 ) {
btn.on("click", function () {
console.log("I want to be fired");
});
}
}
If your widget doesn't have its own view.html than you should let the widget know by adding a view property to the settings object with a value of false.
Here's the paragraph from http://durandaljs.com/documentation/Creating-A-Widget/ that explains that.
Note: In some cases, your widget may not actually need a view. Perhaps it's just adding some jQuery behavior or applying an existing jQuery plugin to a dom element. To tell Durandal that there is no view to load and bind, add a view property to the settings object with a value of false inside your widget's constructor.
In that instance however you can only access elements that are already in the DOM when the widget is instantiated e.g.
var ctor = function (element, settings) {
settings.view = false;
this.btn = $(settings.options.btn);
if ( this.btn.length > 0 ) {
this.btn.on("click", function () {
console.log("I want to be fired");
});
}
};
In my web application, I have a user list in a table on the left, and a user detail pane on the right. When the admin clicks a user in the table, its details should be displayed on the right.
I have a UserListView and UserRowView on the left, and a UserDetailView on the right. Things kind of work, but I have a weird behavior. If I click some users on the left, then click delete on one of them, I get successive javascript confirm boxes for all users that have been displayed.
It looks like event bindings of all previously displayed views have not been removed, which seems to be normal. I should not do a new UserDetailView every time on UserRowView? Should I maintain a view and change its reference model? Should I keep track of the current view and remove it before creating a new one? I'm kind of lost and any idea will be welcome. Thank you !
Here is the code of the left view (row display, click event, right view creation)
window.UserRowView = Backbone.View.extend({
tagName : "tr",
events : {
"click" : "click",
},
render : function() {
$(this.el).html(ich.bbViewUserTr(this.model.toJSON()));
return this;
},
click : function() {
var view = new UserDetailView({model:this.model})
view.render()
}
})
And the code for right view (delete button)
window.UserDetailView = Backbone.View.extend({
el : $("#bbBoxUserDetail"),
events : {
"click .delete" : "deleteUser"
},
initialize : function() {
this.model.bind('destroy', function(){this.el.hide()}, this);
},
render : function() {
this.el.html(ich.bbViewUserDetail(this.model.toJSON()));
this.el.show();
},
deleteUser : function() {
if (confirm("Really delete user " + this.model.get("login") + "?"))
this.model.destroy();
return false;
}
})
I always destroy and create views because as my single page app gets bigger and bigger, keeping unused live views in memory just so that I can re-use them would become difficult to maintain.
Here's a simplified version of a technique that I use to clean-up my Views to avoid memory leaks.
I first create a BaseView that all of my views inherit from. The basic idea is that my View will keep a reference to all of the events to which it's subscribed to, so that when it's time to dispose the View, all of those bindings will automatically be unbound. Here's an example implementation of my BaseView:
var BaseView = function (options) {
this.bindings = [];
Backbone.View.apply(this, [options]);
};
_.extend(BaseView.prototype, Backbone.View.prototype, {
bindTo: function (model, ev, callback) {
model.bind(ev, callback, this);
this.bindings.push({ model: model, ev: ev, callback: callback });
},
unbindFromAll: function () {
_.each(this.bindings, function (binding) {
binding.model.unbind(binding.ev, binding.callback);
});
this.bindings = [];
},
dispose: function () {
this.unbindFromAll(); // Will unbind all events this view has bound to
this.unbind(); // This will unbind all listeners to events from
// this view. This is probably not necessary
// because this view will be garbage collected.
this.remove(); // Uses the default Backbone.View.remove() method which
// removes this.el from the DOM and removes DOM events.
}
});
BaseView.extend = Backbone.View.extend;
Whenever a View needs to bind to an event on a model or collection, I would use the bindTo method. For example:
var SampleView = BaseView.extend({
initialize: function(){
this.bindTo(this.model, 'change', this.render);
this.bindTo(this.collection, 'reset', this.doSomething);
}
});
Whenever I remove a view, I just call the dispose method which will clean everything up automatically:
var sampleView = new SampleView({model: some_model, collection: some_collection});
sampleView.dispose();
I shared this technique with the folks who are writing the "Backbone.js on Rails" ebook and I believe this is the technique that they've adopted for the book.
Update: 2014-03-24
As of Backone 0.9.9, listenTo and stopListening were added to Events using the same bindTo and unbindFromAll techniques shown above. Also, View.remove calls stopListening automatically, so binding and unbinding is as easy as this now:
var SampleView = BaseView.extend({
initialize: function(){
this.listenTo(this.model, 'change', this.render);
}
});
var sampleView = new SampleView({model: some_model});
sampleView.remove();
I blogged about this recently, and showed several things that I do in my apps to handle these scenarios:
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
This is a common condition. If you create a new view every time, all old views will still be bound to all of the events. One thing you can do is create a function on your view called detatch:
detatch: function() {
$(this.el).unbind();
this.model.unbind();
Then, before you create the new view, make sure to call detatch on the old view.
Of course, as you mentioned, you can always create one "detail" view and never change it. You can bind to the "change" event on the model (from the view) to re-render yourself. Add this to your initializer:
this.model.bind('change', this.render)
Doing that will cause the details pane to re-render EVERY time a change is made to the model. You can get finer granularity by watching for a single property: "change:propName".
Of course, doing this requires a common model that the item View has reference to as well as the higher level list view and the details view.
Hope this helps!
To fix events binding multiple times,
$("#my_app_container").unbind()
//Instantiate your views here
Using the above line before instantiating the new Views from route, solved the issue I had with zombie views.
I think most people start with Backbone will create the view as in your code:
var view = new UserDetailView({model:this.model});
This code creates zombie view, because we might constantly create new view without cleanup existing view. However it's not convenient to call view.dispose() for all Backbone Views in your app (especially if we create views in for loop)
I think the best timing to put cleanup code is before creating new view. My solution is to create a helper to do this cleanup:
window.VM = window.VM || {};
VM.views = VM.views || {};
VM.createView = function(name, callback) {
if (typeof VM.views[name] !== 'undefined') {
// Cleanup view
// Remove all of the view's delegated events
VM.views[name].undelegateEvents();
// Remove view from the DOM
VM.views[name].remove();
// Removes all callbacks on view
VM.views[name].off();
if (typeof VM.views[name].close === 'function') {
VM.views[name].close();
}
}
VM.views[name] = callback();
return VM.views[name];
}
VM.reuseView = function(name, callback) {
if (typeof VM.views[name] !== 'undefined') {
return VM.views[name];
}
VM.views[name] = callback();
return VM.views[name];
}
Using VM to create your view will help cleanup any existing view without having to call view.dispose(). You can do a small modification to your code from
var view = new UserDetailView({model:this.model});
to
var view = VM.createView("unique_view_name", function() {
return new UserDetailView({model:this.model});
});
So it is up to you if you want to reuse view instead of constantly creating it, as long as the view is clean, you don't need to worry. Just change createView to reuseView:
var view = VM.reuseView("unique_view_name", function() {
return new UserDetailView({model:this.model});
});
Detailed code and attribution is posted at https://github.com/thomasdao/Backbone-View-Manager
One alternative is to bind, as opposed to creating a series of new views and then unbinding those views. You'd accomplish this doing something like:
window.User = Backbone.Model.extend({
});
window.MyViewModel = Backbone.Model.extend({
});
window.myView = Backbone.View.extend({
initialize: function(){
this.model.on('change', this.alert, this);
},
alert: function(){
alert("changed");
}
});
You'd set the model of myView to myViewModel, which would be set to a User model. This way, if you set myViewModel to another user (i.e., changing its attributes) then it could trigger a render function in the view with the new attributes.
One problem is that this breaks the link to the original model. You could get around this by either using a collection object, or by setting the user model as an attribute of the viewmodel. Then, this would be accessible in the view as myview.model.get("model").
Use this method for clearing the child views and current views from memory.
//FIRST EXTEND THE BACKBONE VIEW....
//Extending the backbone view...
Backbone.View.prototype.destroy_view = function()
{
//for doing something before closing.....
if (this.beforeClose) {
this.beforeClose();
}
//For destroying the related child views...
if (this.destroyChild)
{
this.destroyChild();
}
this.undelegateEvents();
$(this.el).removeData().unbind();
//Remove view from DOM
this.remove();
Backbone.View.prototype.remove.call(this);
}
//Function for destroying the child views...
Backbone.View.prototype.destroyChild = function(){
console.info("Closing the child views...");
//Remember to push the child views of a parent view using this.childViews
if(this.childViews){
var len = this.childViews.length;
for(var i=0; i<len; i++){
this.childViews[i].destroy_view();
}
}//End of if statement
} //End of destroyChild function
//Now extending the Router ..
var Test_Routers = Backbone.Router.extend({
//Always call this function before calling a route call function...
closePreviousViews: function() {
console.log("Closing the pervious in memory views...");
if (this.currentView)
this.currentView.destroy_view();
},
routes:{
"test" : "testRoute"
},
testRoute: function(){
//Always call this method before calling the route..
this.closePreviousViews();
.....
}
//Now calling the views...
$(document).ready(function(e) {
var Router = new Test_Routers();
Backbone.history.start({root: "/"});
});
//Now showing how to push child views in parent views and setting of current views...
var Test_View = Backbone.View.extend({
initialize:function(){
//Now setting the current view..
Router.currentView = this;
//If your views contains child views then first initialize...
this.childViews = [];
//Now push any child views you create in this parent view.
//It will automatically get deleted
//this.childViews.push(childView);
}
});
It appears as though the following code is getting inside initialize but my event doesn't appear to be firing.
What am I missing here?
var index = (function ($, window, document) {
var methods = {};
methods = {
init: function () {
},
getView: Backbone.View.extend({
el: $('.settings'),
events: {
'click .settings': 'addUl'
},
initialize: function () {
console.log('init');
},
render: function () {
},
addUl: function () {
console.log('addUI');
this.el.append("<ul> <li>hello world </li> </ul>");
}
})
};
return methods; } (jQuery, window, document));
var stuff = new index.getView();
Link to the jsbin
Remove the space in 'click .settings'
Actually remove .settings entirely.
'click .settings' is registering a click handler for a descendant of this.el that matches '.settings'.
In your example you want to register an event on this.el directly so you don't need the descendant selector.
The problem is that it is your view element ($el) that has the settings class and not a child.
click .settings tells backbone to bind a "click" event on the $el for any children that have .settings. However, because, it is $el which has the class settings the binding never match.
This is why when you remove .settings it works, because you say "any 'click' on $el"
The reason the documentation says click .blah is because it assumes that the html element(s) with the class='blah' are children of the $el element.
Hope this help.