I'm trying to use Backbone.js for the first time and I'm having some trouble. I don't know if my problem is that I'm not understanding how backbone is supposed to work or if it's just a code problem.
I'm trying to create a dynamic menu, and I have no problem creating the main menu bar with it's items, but I can't get the hover event to trigger whenever I hover one of the menu items.
Views
var MenuView = Backbone.View.extend({
initialize: function(items) {
this.menu = items;
//Main navigation bar
this.el = $("#main-nav");
this.trigger('start');
this.render();
},
render: function() {
var me = this;
_.each(this.menu, function(mi) {
mi.render(me.el);
});
return this;
},
handleHover: function(e) {
console.debug(e);
}
});
var MenuItemView = Backbone.View.extend({
tagName: 'li',
className:'menu-item',
events: { //none of these work
'hover a':'handleHover',
'mouseover a':'handleHover',
'mouseover':'handleHover',
'click': 'handleHover',
'click a': 'handleHover'
},
initialize: function(mi) {
this.menuItem = mi;
this.el = $("<li class=\"menu-item\"></li>")
},
render: function(parent) {
this.el.append('' + this.menuItem.get("text") + '');
parent.append(this.el);
return this;
},
handleHover: function(ev) {
console.debug("Hovering! " + ev + this.menuItem.get("cid"));
console.debug(ev);
return false;
}
});
Model
var MenuItem = Backbone.Model.extend({
defaults: {
parent: null,
children: [],
link: "",
text: ""
}
});
Startup code
$(document).ready(function() {
var menu = new MenuView([
new MenuItemView( new MenuItem({link: "/", text: "Home"})),
new MenuItemView( new MenuItem({link: "/", text: "Users"})),
new MenuItemView( new MenuItem({link: "/", text: "Configuration"}))
]);
});
Any help will be appreciated!
Thanks!
Update
Ok, after taking the definition of el outside of the initialize method on the MenuItemView view, it works, BUT that same element gets reused on all instances of the view, so I had to change the view to the following code in order to make it work the way I want it:
var MenuItemView = Backbone.View.extend({
events: { //none of these work
'hover a':'handleHover',
'mouseover a':'handleHover',
'mouseover':'handleHover',
'click': 'handleHover',
'click a': 'handleHover'
},
el: $('<li class="menu-item"></li>'),
initialize: function(mi) {
this.menuItem = mi;
this.el = $(this.el).clone(true);
},
render: function(parent) {
this.el.append('' + this.menuItem.get("text") + '');
parent.append(this.el);
return this;
},
handleHover: function(ev) {
console.debug("Hovering! " + ev + this.menuItem.get("cid"));
console.debug(ev);
return false;
}
});
Wny do I have to clone the element on a new instance?
hover is not a normal event, but a 'convenience' event provided by jquery. It is a combination of mouseenter and mouseleave.
Binding to mouseenter and mouseleave instead of hover will do what you need.
Re: "Why do I have to clone the element on a new instance?"
The underlying problem is right here:
var MenuItemView = Backbone.View.extend({
// ...
el: $('<li class="menu-item"></li>'),
The $('<li class="menu-item"></li>') call is executed when MenuItemView is being defined so you end up with only one $('<li>') being shared across all instances of MenuItemView.
If you create the el inside initialize or render then you'll have to bind the events by hand using delegateEvents:
By default, delegateEvents is called within the View's constructor for you [...]
So if you create this.el yourself then you'll have to call this.delegateEvents() yourself. For example:
var MenuItemView = Backbone.View.extend({
// ...
render: function() {
this.el = $('<li class="menu-item"><a>' + this.cid + '</a></li>');
this.delegateEvents();
return this;
},
//...
});
Demo: http://jsfiddle.net/ambiguous/RPqMh/2/
However, if you clone your this.el with the withDataAndEvents flag on, then you should be fine:
var MenuItemView = Backbone.View.extend({
el: $('<li class="menu-item"></li>'),
// ...
initialize: function() {
this.el = this.el.clone(true);
this.el.append('<a>' + this.cid + '</a>');
},
//...
});
Demo: http://jsfiddle.net/ambiguous/hCW3F/1/
But if you just this.el.clone(), it won't work because the delegate won't be bound to the clone:
var MenuItemView = Backbone.View.extend({
el: $('<li class="menu-item"></li>'),
// ...
initialize: function() {
this.el = this.el.clone();
this.el.append('<a>' + this.cid + '</a>');
},
// ...
});
Demo: http://jsfiddle.net/ambiguous/KZNPA/
But if you add your own delegateEvents call, you'll be okay:
var MenuItemView = Backbone.View.extend({
el: $('<li class="menu-item"></li>'),
// ...
initialize: function() {
this.el = this.el.clone();
this.el.append('<a>' + this.cid + '</a>');
},
render: function() {
this.delegateEvents();
return this;
},
// ...
});
Demo: http://jsfiddle.net/ambiguous/KZNPA/1/
It seems to me that you don't need this properties:
tagName: 'li',
className:'menu-item'
in MenuItemView if you specify this.el = $('<li class="menu-item"></li>');
Related
I was trying to add click event handler to my backbone javascript file. But it displays an error : "Uncaught TypeError: Cannot read property 'html' of undefined."
Here is the code. Any help would be appreciated.
main.js file
var Song = Backbone.Model.extend();
var SongView = Backbone.Model.extend({
events: {
"click": "onClick",
"click .bookmark": "onClickBookmark"
},
onClick: function(){
console.log("Listen Clicked");
},
onClickBookmark: function(e){
e.stopPropagation();
console.log("Bookmark Clicked");
},
render: function(){
this.$el.html(this.model.get("title") + " <button>Listen</button> <button class='bookmark'> Bookmark</button>");
return this;
}
});
var song = new Song({title: "sky is sdv sv"});
var songView = new SongView({el: "#container", model: song});
songView.render();
var SongView = Backbone.Model.extend({
should be
var SongView = Backbone.View.extend({
I've got a problem with my Backbone.js app (Fiddle: http://jsfiddle.net/the_archer/bew7x010/3/). The app is list (ol element) with nested lists within. Here's my starting HTML:
<body>
<ol id="flowList"></ol>
</body>
<script type="text/template" id="item-template">
<%= content %>
</script>
When the list is empty. I add a new <li> element within it using Backbone.js and focus on the li.
When within the li, if I press the enter key, I want to insert a new li just after the li I pressed enter within.
If I press tab, I want to add a new sub ol element within the list element. I handle the keypress within the li elements like so:
handleKeyboardShortcuts: function(e){
if (e.keyCode == 13 && !e.shiftKey){
e.preventDefault();
this.el = $(e.target).parent();
Items.create({contnet: "New Item!"});
}
}
I have a listenTo on my collection, which on add, appends a li to the #flowList ol element using the addOne function:
addOne: function(todo) {
var view = new ItemView({model: todo});
$(this.el).append(view.render().el);
}
My problem is:
How do I pass the target element to add to, to the addOne function?
How do I pass an option to the addOne function, which instead of doing $(this.el).append does $(this.el).after ?
For some reason, I can't get my head around how to pass around those details. Here's the full Backbone.js code:
$(function() {
var Item = Backbone.Model.extend({
defaults: function() {
return {
content: "empty item..."
};
}
});
var ItemList = Backbone.Collection.extend({
model: Item,
localStorage: new Backbone.LocalStorage("todos-backbone"),
});
var Items = new ItemList;
var ItemView = Backbone.View.extend({
tagName: "li",
template: _.template($('#item-template').html()),
events: {
"click": "enableEdit",
"blur": "disableEdit",
},
initialize: function() {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
enableEdit: function(){
this.$el.attr("contenteditable","true").focus();
},
disableEdit: function(){
this.$el.attr("contenteditable","false");
}
});
var AppView = Backbone.View.extend({
el: $("#flowList"),
events: {
"keydown li": "handleKeyboardShortcuts"
},
initialize: function() {
this.listenTo(Items, 'add', this.addOne);
this.listenTo(Items, 'reset', this.addAll);
this.listenTo(Items, 'all', this.render);
Items.fetch();
if (Items.length === 0){
Items.create({content: "Sample Item!"});
}
},
render: function(e) {
console.log(e);
},
addOne: function(todo) {
var view = new ItemView({model: todo});
$(this.el).append(view.render().el);
},
addAll: function() {
Items.each(this.addOne, this);
},
handleKeyboardShortcuts: function(e){
if (e.keyCode == 13 && !e.shiftKey){
e.preventDefault();
this.el = $(e.target).parent();
Items.create({contnet: "New Item!"});
}
}
});
var App = new AppView;
});
Here's a link to the fiddle: http://jsfiddle.net/the_archer/bew7x010/3/
You could keep track of the selected item, enabling you to insert the new one after or within it.
In your existing enableEdit handler, trigger an 'item:select' event:
var ItemView = Backbone.View.extend({
enableEdit: function(){
this.trigger('item:select', this);
this.$el.attr("contenteditable","true").focus();
}
});
Then in your AppView, when you add a new item, attach a listener for the new item that updates the value of a 'selectedItem' property.
Then you can insert the new item into the dom based on the state of that property:
var AppView = Backbone.View.extend({
addOne: function(todo) {
var view = new ItemView({model: todo});
this.listenTo( view, 'item:select', this.handleItemSelect );
if(this.selectedItem) {
$(this.selectedItem.el).after(view.render().el);
}
else {
$(this.el).append(view.render().el);
}
},
handleItemSelect: function(item) {
console.log('item selected:', item);
this.selectedItem = item;
}
});
You should be able to do something similar for the tab key behaviour, by setting a flag when the tab key is pressed, before calling Items.create.
In the events hash, i want to use resize event, but they
do not work neither. However, click event works well. why resize does not work?
var SubheaderView = Backbone.View.extend({
id: 'gallery',
tagName: 'div',
events: {
'click #minor': 'getPadding',
'resize #minor': 'getPadding' //not work
},
initialize: function() {
this.render();
},
render: function() {
this.$el.append(subheaderTemplate);
},
getPadding: function() {
var pad_top = Math.floor($('#minor').height() * 0.4);
var pad_left = Math.floor($('#minor').width() * 0.07);
var pad = pad_top + 'px 0px 0px ' + pad_left + 'px';
$('#cover h1').css({'padding': pad});
}
});
It does not related to Backbone at all. According to MDN, only window has a resize event.
You can do something like this:
Add this code to backbone.js file you are using
Backbone.View.prototype.eventAggregator = _.extend({}, Backbone.Events);
$(window).resize(function () {
Backbone.View.prototype.eventAggregator.trigger('window:resize');
});
Then bind the event aggregator to any view to want to use the resize event
this.eventAggregator.bind('window:resize',this.resize)
where this.resize is the function to be called for the resize event
I have base class which extends Backbone.View class.
I pass a Jquery DOM element through the constructor, is this object a candidate for garbage collection?
var MainView = BaseView.extend({
initialize: function(o) {
this.container = o.myJqueryDomElement;
this.render();
},
render: function() {
this.container.append(this.$el);
},
dispose: function() {
this.remove();
}
});
in some point in my code I do this:
var mainView = new MainView({
myJqueryDomElement = $('#content')
});
first of all you should pass the jQuery element in as el, so
var MainView = BaseView.extend({
initialize: function(o) {
this.render();
},
render: function() {
this.html(someTemplate)
},
dispose: function() {
this.remove();
}
});
var mainView = new MainView({
el: $('#content')
});
Then this will be available throughout the object as a jQuery object via this.$el and the view shouldn't know about its container just the element it lives in. So if you want this to be the main view change your selector to be $('#content .main') or just $('#main')
So here is my view:
$(function() {
var ImageManipulation = Backbone.View.extend({
el: $('body'),
tagName: "img",
events: {
'mouseover img': 'fullsize',
'click img#current': 'shrink'
},
initialize: function() {
_.bindAll(this, 'render', 'fullsize', 'shrink');
//var message = this.fullsize;
//message.bind("test", this.fullsize);
},
render: function() {
},
fullsize: function() {
console.log("in fullsize function");
console.log(this.el);
$('.drop-shadow').click(function() {
console.log(this.id);
if (this.id != 'current') {
$('.individual').fadeIn();
$(this).css('position', 'absolute');
$(this).css('z-index', '999');
$(this).animate({
top: '10px',
height: '432px',
}, 500, function() {
this.id = "current";
console.log("animation complete");
return true;
});
};
});
},
shrink: function() {
$('.individual').fadeOut();
$('#current').animate({
height: '150px',
}, 500, function() {
this.id = "";
$(this).css('position', 'relative');
$(this).css('z-index', '1');
console.log("animation complete");
return true;
});
}
});
var startImages = new ImageManipulation();
});
What I don't understand is how to change the el to make 'this' take over the click function I have in full-size. I would much rather have the click jQuery function removed and have the mouseover function be another click, but I cant seem to figure out how to assign 'this' to the particular image that is being clicked. I hope my question makes sense.
Backbone's event handler assumes that you want to know about the object (both its code, and its DOM representation, the View.el object) for every event, and that the event is intended to change some aspect of the view and/or model. The actual target of the click is something you're assumed to know, or assumed to be able to derive.
Derivation is rather simple:
fullsize: function(ev) {
target = $(ev.currentTarget);
And replace all your this. references within your call to target.. this. will continue to refer to the View instance. In your inner function, the anonymous one assigned to .drop-shadow, this. will refer to the object that was just clicked on. If you want access to the surrounding context, use the closure forwarding idiom:
fullsize: function(ev) {
var target = ev.currentTarget;
var self = this;
$('.drop-shadow').click(function(inner_ev) {
console.log(this.id); // the same as inner_ev.currentTarget
console.log(self.cid); // the containing view's CID