I've looked around for a while now and have not been able to find anything that suggests what the cause of this is.
My code:
var faqView = Backbone.View.extend({
tagName: 'div',
id: 'faq-list',
initialize: function() {
var view = this;
this.collection = new faqCollection();
this.collection.fetch({
success: function(collection, response) {
collection.each(function(faq){
view.$el.append(_.template($('script#faq_item').html(),{faq: faq.attributes}));
});
},
error: function(collection, response) {
view.$el.html("<p>Unable to get the FAQ items.<br>Please try again later.</p>");
}
});
},
render: function() {
this.$el.appendTo('div#container');
return this;
},
events: {
'click h3': 'toggleAnswer'
},
toggleAnswer: function(event) {
console.log(this);
console.log(event);
}
});
var router = Backbone.Router.extend({
routes: {
"faq": "faq",
"*other": "defaultRoute"
},
faqView: {},
initialize: function() {
this.faqView = new faqView();
},
defaultRoute: function() {
this.resetPage();
},
faq: function() {
this.resetPage();
$('body').addClass('page-faq');
this.faqView.render();
},
resetPage: function() {
$('body').removeClass('page-faq');
this.faqView.remove();
}
});
The above code is included as the last items in the <body>. The HTML is as follows.
<body>
<div id="container">
</div>
<script type="text/template" id="faq_item">
<h3 class="contracted"><span>{{faq.question}}</span></h3>
<p style="display: none;">{{faq.answer}}</p>
</script>
<script type="text/javascript" src="./js/models.js"></script>
<script type="text/javascript" src="./js/views.js"></script>
<script type="text/javascript" src="./js/collection.js"></script>
<script type="text/javascript" src="./js/router.js"></script>
<script type="text/javascript">
//<![CDATA[
$(function() {
var app = new router;
Backbone.history.start();
});
//]]>
</script>
</body>
All the required elements exist (as far as I can tell) and I'm not manually setting the el attribute of the View. I'm lost as to why the events are not binding/firing when the <h3> is clicked.
Edit No errors thrown and the functionality works if I don't use the router and create the view by it self. e.g.
var app = new faqView();
app.render();
Your problem is in your router. Right here in fact:
resetPage: function() {
$('body').removeClass('page-faq');
this.faqView.remove();
}
View#remove is just jQuery's remove on the view's el by default and that:
[...] method takes elements out of the DOM. [...] all bound events and jQuery data associated with the elements are removed
So once you this.faqView.remove(), the delegate handler that drives the view's events is gone.
The usual approach is to create and destroy views as needed instead of creating a view and caching it for later. Your router should look more like this:
var router = Backbone.Router.extend({
routes: {
"faq": "faq",
"*other": "defaultRoute"
},
defaultRoute: function() {
this.resetPage();
},
faq: function() {
this.resetPage();
$('body').addClass('page-faq');
this.view = new faqView();
this.view.render();
},
resetPage: function() {
$('body').removeClass('page-faq');
if(this.view)
this.view.remove();
}
});
Demo: http://jsfiddle.net/ambiguous/aDtDT/
You could try messing around with detach inside an overridden remove method in faqView as well but there's really no need to have an instance of faqView around all the time: create it when you need it and remove it when you don't.
Related
I have an editor view and if there are unsaved changed I am prompting on window closes and also on backbone routes.
Problem is that Backbone.Router.execute runs after the url change and so I am trying to implement the most reliable and elegant way of preventing the url change.
In the example below clicking the "About" route will prevent the route callback and then rewind the url change - it seems less than ideal that I have to use window.history.back() (because it creates a history entry).
Can you think of a better way? I know a jQuery on-click can catch the event before url change but I'm not sure how to nicely integrate that with a Backbone.Router. Thanks.
var HomeView = Backbone.View.extend({
template: '<h1>Home</h1>',
initialize: function () {
this.render();
},
render: function () {
this.$el.html(this.template);
}
});
var AboutView = Backbone.View.extend({
template: '<h1>About</h1>',
initialize: function () {
this.render();
},
render: function () {
this.$el.html(this.template);
}
});
var ContactView = Backbone.View.extend({
template: '<h1>Contact</h1>',
initialize: function () {
this.render();
},
render: function () {
this.$el.html(this.template);
}
});
var AppRouter = Backbone.Router.extend({
routes: {
'': 'homeRoute',
'home': 'homeRoute',
'about': 'aboutRoute',
'contact': 'contactRoute'
},
execute: function(callback, args, name) {
if (window.location.hash === '#/about') {
window.history.back();
return false;
}
if (callback) {
callback.apply(this, args);
}
},
homeRoute: function () {
var homeView = new HomeView();
$("#content").html(homeView.el);
},
aboutRoute: function () {
var aboutView = new AboutView();
$("#content").html(aboutView.el);
},
contactRoute: function () {
var contactView = new ContactView();
$("#content").html(contactView.el);
}
});
var appRouter = new AppRouter();
Backbone.history.start();
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="http://underscorejs.org/underscore.js"></script>
<script src="http://backbonejs.org/backbone.js"></script>
<div id="navigation">
Home
About
Contact
</div>
<div id="content"></div>
The only thing I can think of is listening to clicks and doing things with jQuery, or saving the last hash and doing window.history.replaceState(undefined, undefined, "#last_hash_value").
I am rendering a Backbone collection as a select element, and I want it to have a blank option at the top.
I am a bit unsure how to best do this using a Marionette CollectionView. (I am aware of the CompositeView but I don't really feel it fits this usecase).
So far I've tried using the onBeforeRender hook to add the option
onBeforeRender: function() {
var option = $('<option>').html('')
this.$el.append(option);
}
The problem I'm facing is that render is actually being called twice, since my CollectionView is inside a LayoutView which calls the render function when it shows the view
this.getRegion('body').show(new SelectView({
collection: collection
}));
And then render is triggered again when its collection is reset
collection.reset([{ name: 'a' },{ name: 'b' }]);
I've verified that this is what's happening by using the debugger and looking at the callstack.
Complete snippet:
$(document).ready(function() {
var collection = new Backbone.Collection({
model: Backbone.Model
});
var Layout = Backbone.Marionette.LayoutView.extend({
regions: {
body: '#main'
},
template: false,
onRender: function() {
this.getRegion('body').show(new SelectView({
collection: collection
}));
}
});
var OptionView = Backbone.Marionette.ItemView.extend({
tagName: 'option',
template: _.template('<%= name %>')
});
var SelectView = Backbone.Marionette.CollectionView.extend({
tagName: 'select',
childView: OptionView,
onBeforeRender: function() {
var option = $('<option>').html('')
this.$el.append(option);
}
});
var layout = new Layout({
el: $('#app')
});
layout.render();
var app = new Marionette.Application();
app.start();
Backbone.history.start();
collection.reset([{ name: 'a' },{ name: 'b' }]);
});
<script data-require="underscore.js#1.6.0" data-semver="1.6.0" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script>
<script data-require="jquery#2.1.3" data-semver="2.1.3" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script data-require="backbone.js#1.1.2" data-semver="1.1.2" src="http://backbonejs.org/backbone-min.js"></script>
<script data-require="marionette.js#2.2.2" data-semver="2.2.2" src="http://cdnjs.cloudflare.com/ajax/libs/backbone.marionette/2.2.2/backbone.marionette.js"></script>
<body>
<div id="app">
<div id="main"></div>
</div>
</body>
Now in this snippet I could fix the issue by simply resetting the collection at the start of the script instead of at the end, but in my application that is not a possible solution.
I can think of a few solutions to this, but they all feel a bit wrong, I most certainly don't want to maintain any state for this in my view. The least worst thing I could think of was to change to
onBeforeRender: function() {
this.$el.html('')
var option = $('<option>').html('')
this.$el.append(option);
}
But since the framework doesn't seem to empty the $el itself I am guessing there's a reason for that.
Is this a legit usecase for CompositeView or is there some other smart way to do this that I have missed?
In the case that CompositeView is OK for this usecase, the solution would be to change the CollectionView into
var SelectView = Backbone.Marionette.CompositeView.extend({
template: _.template('<select><option></option></select>'),
childView: OptionView,
childViewContainer: 'select'
});
As demonstrated in this snippet:
$(document).ready(function() {
var collection = new Backbone.Collection({
model: Backbone.Model
});
var Layout = Backbone.Marionette.LayoutView.extend({
regions: {
body: '#main'
},
template: false,
onRender: function() {
this.getRegion('body').show(new SelectView({
collection: collection
}));
}
});
var OptionView = Backbone.Marionette.ItemView.extend({
tagName: 'option',
template: _.template('<%= name %>')
});
var SelectView = Backbone.Marionette.CompositeView.extend({
template: _.template('<select><option></option></select>'),
childView: OptionView,
childViewContainer: 'select'
});
var layout = new Layout({
el: $('#app')
});
layout.render();
var app = new Marionette.Application();
app.start();
Backbone.history.start();
collection.reset([{ name: 'a' },{ name: 'b' }]);
});
<script data-require="underscore.js#1.6.0" data-semver="1.6.0" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script>
<script data-require="jquery#2.1.3" data-semver="2.1.3" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script data-require="backbone.js#1.1.2" data-semver="1.1.2" src="http://backbonejs.org/backbone-min.js"></script>
<script data-require="marionette.js#2.2.2" data-semver="2.2.2" src="http://cdnjs.cloudflare.com/ajax/libs/backbone.marionette/2.2.2/backbone.marionette.js"></script>
<body>
<div id="app">
<div id="main"></div>
</div>
</body>
I am learning the backbone currently, and facing the following problem.
I want to append new elements to the this.el element of the View. But it doesn't happen.
here is my code:
var MyData = Backbone.Model.extend({
initialize: function() {
this.bind('error', function(model, error) {
console.log('error:' + error);
});
},
defaults: {
name: "Jo",
age: 18,
skill: 0
},
validate: function(attributes) {
if (attributes.name == "Jonh")
return false;
return true;
}
});
var MyView1 = Backbone.View.extend({
initialize: function() {
this.$el.empty();
},
el: '#middle',
events: {
"click": "render"
},
render: function() {
this.$el.append(this.model.get('name'));
}
});
var myData = new MyData();
var myView1 = new MyView1( {model: myData} );
int the html file there is a div element defined as follows:
<div id="middle"></div>
The code works without error, but I dont see any appended elements.
I have also tried to append like this:
this.$el.append($("<p>").append(this.model.get('name')));
Since your javascript script loads before the page is loaded, Backbone View can't see #middle div, so you have to initialize views after page is loaded:
$(document).ready(function() {
// load your views
});
I just loaded this up in my IDE and it worked fine. You console didn't show you any errors?
So for some reason navigate won't work in one of my views. I'm doing everything in one file for now, so that may be the problem. Also I know the code is horrible, I'm just messing around with backbone right now.
EDIT: I put a console.log() in MarketingPage's function route and it never gets called, so there must be something wrong with the view.
Also, this is the error I'm getting from chrome dev tools:
Error in event handler for 'undefined': IndexSizeError: DOM Exception 1 Error: Index or size was negative, or greater than the allowed value.
at P (chrome-extension://mgijmajocgfcbeboacabfgobmjgjcoja/content_js_min.js:16:142)
at null.<anonymous> (chrome-extension://mgijmajocgfcbeboacabfgobmjgjcoja/content_js_min.js:18:417)
at chrome-extension://mgijmajocgfcbeboacabfgobmjgjcoja/content_js_min.js:1:182
at miscellaneous_bindings:288:9
at chrome.Event.dispatchToListener (event_bindings:390:21)
at chrome.Event.dispatch_ (event_bindings:376:27)
at chrome.Event.dispatch (event_bindings:396:17)
at Object.chromeHidden.Port.dispatchOnMessage (miscellaneous_bindings:254:22)
Here's my code:
/*global public, $*/
window.public = {
Models: {},
Collections: {},
Views: {},
Routers: {
},
init: function () {
console.log('Hello from Backbone!');
}
};
var App = Backbone.Router.extend({
routes: {
'': 'index',
'register': 'route_register',
},
index: function(){
var marketing_page = new MarketingPage();
},
route_register: function(){
var register_view = new RegisterView();
}
});
window.app = new App();
var User = Backbone.Model.extend({
url: '/user',
defaults: {
email: '',
password: ''
}
});
var MarketingPage = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
var template = _.template($("#marketing-page").html());
$('.search-box').after(template);
},
events: {
'dblclick': 'route'
},
route: function(e){
e.preventDefault();
console.log("In route");
window.app.navigate('register', {trigger: true});
this.remove();
}
});
var RegisterView = Backbone.View.extend({
initialize: function() {
this.render();
},
render: function(){
var template = _.template($("#register-template").html());
$('.search-box').after(template);
}
});
$(document).ready(function () {
Backbone.history.start();
});
When I type host/#register into the browser directly, the register view gets rendered, but no matter what I do the click event won't seem to work...
Since the handler function route isn't being called, it's likely that the event delegation isn't working.
One thing to note is that the event handling that is set up in a Backbone View is scoped to only that view's el. I don't see where yours is set up explicitly, so it might be creating an empty div, then handling events inside that empty div (which you don't want).
One trick I use for quick prototypes is to set the view's el with a jQuery selector pointing to something that exists on the page already, then in the render, show it with a .show().
Since you're not really doing that, here's one thing you could try. What we're doing is setting the $el content and then calling delegateEvents to make sure that the events and handlers are being bound.
var MarketingPage = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
this.$el.html(_.template($("#marketing-page").html()));
$('.search-box').after(this.$el);
this.delegateEvents();
},
events: {
'dblclick': 'route'
},
route: function(e){
e.preventDefault();
console.log("In route");
window.app.navigate('register', {trigger: true});
this.remove();
}
});
Backbone.js views delegateEvents do not get bound (sometimes)
http://backbonejs.org/#View-delegateEvents
I'm starting to learn backbone.js and I've built my first page and I want to know If I'm going down the 'correct' path (as much as there is ever a correct path in software).
Is it possible to get the model properties (attributes) to automatically bind to the html elements?
The html:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>settings page</title>
<link rel="stylesheet" type="text/css" href="../Content/theme.css" />
<script language="javascript" src="../Scripts/jquery-1.7.1.js"></script>
<script language="javascript" src="../Scripts/underscore.js"></script>
<script language="javascript" src="../Scripts/backbone.js"></script>
<script language="javascript" src="../Scripts/settings.js"></script>
</head>
<body>
<div style="width:95%;margin:10px;padding:10px;background-color:#ffffff;color:#000000;padding-bottom:8px;padding-right:5px;padding-top:4px;float:left;">
<h1>
Settings...
</h1>
Server URL (cloud based API):
<br />
<input id="settings-service-url" type="text" size="100" />
<br />
<br />
Timeout:
<br />
<input id="settings-timeout" type="text" size="100" />
<br />
<br />
<button id="update-settings">Update Settings</button>
</div>
</body>
</html>
Javascript:
$(document).ready(function () {
if (typeof console == "undefined") {
window.console = { log: function () { } };
}
Settings = Backbone.Model.extend({
defaults: {
ServiceUrl: "",
Timeout: 0
},
url: function () {
return '/settings';
},
replaceServiceUrlAttr: function (url) {
this.set({ WisdomServiceUrl: url });
},
replaceTimeoutAttr: function (timeout) {
this.set({ Timeout: timeout });
}
});
SettingsView = Backbone.View.extend({
tagName: 'li',
events: {
'click #update-settings': 'updateSettings'
},
initialize: function () {
_.bindAll(this, 'render');
this.settings = new Settings;
this.settings.fetch({ success: function () {
view.render(view.settings);
}
});
},
updateSettings: function () {
view.settings.replaceServiceUrlAttr($('#settings-service-url').val());
view.settings.replaceTimeoutAttr($('#settings-timeout').val());
view.settings.save();
},
render: function (model) {
$('#settings-wisdom-service-url').val(model.get("WisdomServiceUrl"));
$('#settings-timeout').val(model.get("Timeout"));
}
});
var view = new SettingsView({ el: 'body' });
});
There are a mistake in your view. First of all, it's common practice to pass the model as parameter when you create a new view:
var view = new SettingsView({ "el": "body", "model": new Settings() });
now you can access your model by this.model in your view.
Next thing is the use of the variable view in your view. Using Backbone's View means you can have multiple instances of one View class. So calling new SettingsView() creates an instance of your view. Let's think about having two instances of your view:
var view = new SettingsView({ "el": "body", "model": new Settings() });
var view1 = new SettingsView({ "el": "body", "model": new Settings() });
Whenever you call view.settings.save(); in one of your instances it will always call the method in the first view instance because it's bound the variable name "view". So all you have to do use this instead of view:
SettingsView = Backbone.View.extend({
tagName: 'li',
events: {
'click #update-settings': 'updateSettings'
},
initialize: function () {
this.settings = new Settings;
this.settings.fetch({ success: _.bind(function () {
//to get this work here we have to bind "this",
//otherwise "this" would be the success function itself
this.render(view.settings);
}, this)
});
},
updateSettings: function () {
this.model.replaceServiceUrlAttr($('#settings-service-url').val());
this.model.replaceTimeoutAttr($('#settings-timeout').val());
this.model.save();
},
render: function () {
$('#settings-wisdom-service-url').val(this.model.get("WisdomServiceUrl"));
$('#settings-timeout').val(this.model.get("Timeout"));
}
});
Using both settings methods in your model doesn't make much sense at the moment as they just call set. So you could call set on the model directly.
Also using tagName: 'li' and inserting an element will not work as you expected. Using tagName only has an effect if you don't insert an element into the constructor. In this case backbone will create a new element using the tagName. Otherwise the element of the view is the one you passed into the constructor.