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").
Related
I am creating a view but it isn't rendering in my page.
I am building a SPA with backbone, and I need that my template can open inside of div in my body, but I don't know what is my problem here.
What can be?
Show this error:
Uncaught TypeError: Cannot read property '_listenId' of undefined
at child.Events.(anonymous function) [as listenTo] (http://localhost:9000/bower_components/backbone/backbone.js:222:19)
at child.initialize (http://localhost:9000/scripts/views/RepositoriesView.js:21:12)
at child.Backbone.View (http://localhost:9000/bower_components/backbone/backbone.js:1001:21)
at new child (http://localhost:9000/bower_components/backbone/backbone.js:1566:41)
at child.repositories (http://localhost:9000/scripts/routes/AppRouter.js:46:7)
at child.execute (http://localhost:9000/bower_components/backbone/backbone.js:1265:30)
at Object.callback (http://localhost:9000/bower_components/backbone/backbone.js:1254:16)
at http://localhost:9000/bower_components/backbone/backbone.js:1481:19
at Function.some (http://localhost:9000/bower_components/lodash/dist/lodash.compat.js:4304:25)
at Backbone.History.loadUrl (http://localhost:9000/bower_components/backbone/backbone.js:1479:16)
My AppRouter is:
/*global Sice, Backbone*/
Sice.Routers = Sice.Routers || {};
Sice.Views = Sice.Views || {};
(function() {
'use strict';
Sice.Routers.AppRouter = Backbone.Router.extend({
//map url routes to contained methods
routes: {
"": "repositories",
"repositories": "repositories",
"search": "search",
"starreds": "starreds"
},
deselectPills: function() {
//deselect all navigation pills
$('ul.pills li').removeClass('active');
},
selectPill: function(pill) {
//deselect all navigation pills
this.deselectPills();
//select passed navigation pill by selector
$(pill).addClass('active');
},
hidePages: function() {
//hide all pages with 'pages' class
$('div#content').hide();
},
showPage: function(page) {
//hide all pages
this.hidePages();
//show passed page by selector
$(page).show();
},
repositories: function() {
this.showPage('div#content');
this.selectPill('li.repositories-pill');
new Sice.Views.RepositoriesView();
},
search: function() {
this.showPage('div#content');
this.selectPill('li.search-pill');
},
starreds: function() {
this.showPage('div#content');
this.selectPill('li.starreds-pill');
}
});
Sice.Views.AppView = Backbone.View.extend({
//bind view to body element (all views should be bound to DOM elements)
el: $('body'),
//observe navigation click events and map to contained methods
events: {
'click ul.pills li.repositories-pill a': 'displayRepositories',
'click ul.pills li.search-pill a': 'displaySearch',
'click ul.pills li.starreds-pill a': 'displayStarreds'
},
//called on instantiation
initialize: function() {
//set dependency on Sice.Routers.AppRouter
this.router = new Sice.Routers.AppRouter();
//call to begin monitoring uri and route changes
Backbone.history.start();
},
displayRepositories: function() {
//update url and pass true to execute route method
this.router.navigate("repositories", true);
},
displaySearch: function() {
//update url and pass true to execute route method
this.router.navigate("search", true);
},
displayStarreds: function() {
//update url and pass true to execute route method
this.router.navigate("starreds", true);
}
});
//load application
new Sice.Views.AppView();
})();
My View is:
/*global Sice, Backbone, JST*/
Sice.Views = Sice.Views || {};
(function() {
'use strict';
Sice.Views.RepositoriesView = Backbone.View.extend({
template: JST['app/scripts/templates/RepositoriesView.ejs'],
tagName: 'div',
id: 'repositoriesView',
className: 'page-repositories',
events: {},
initialize: function() {
this.listenTo(this.model, 'change', this.render);
},
render: function() {
this.$el.html(this.template());
}
});
})();
What's happening?
In the following router function, you're instantiating a new View instance.
repositories: function() {
this.showPage('div#content');
this.selectPill('li.repositories-pill');
new Sice.Views.RepositoriesView(); // <-- here
},
But you're not passing a model object which the view listens to.
initialize: function() {
this.listenTo(this.model, 'change', this.render);
},
So it's calling this.listenTo with this.model as undefined.
What's the solution?
Pass a model instance
new Sice.Views.RepositoriesView({ model: new MyModel() });
Create a model instance in the view
initialize: function() {
this.model = new MyModel();
this.listenTo(this.model, 'change', this.render);
},
I have several Backbone Models rendered in a Collection View, and also I have a route that should render a view of that model. So, here come the views
resume.js
// this renders a single model for a collection view
var ResumeView = Backbone.View.extend({
model: new Resume(),
initialize: function () {
this.template = _.template($('#resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON));
return this;
}
});
#resume template
<section id="resume">
<h1><%= profession %></h1>
<!-- !!!!! The link for a router which should navigate to ShowResume view -->
View Details
</section>
Collection view:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection;
this.collection.on('add', this.render, this);
// Getting the data from JSON-server
this.collection.fetch({
success: function (res) {
_.each(res.toJSON(), function (item) {
console.log("GET a model with " + item.id);
});
},
error: function () {
console.log("Failed to GET");
}
});
},
render: function () {
var self = this;
this.$el.html('');
_.each(this.collection.toArray(), function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
}
});
The code above works perfectly and does exactly what I need -- an array of models is fetched from my local JSON-server and each model is displayed within a collection view. However, the trouble starts when I try to navigate through my link in the template above. Here comes the router:
var AppRouter = Backbone.Router.extend({
routes: {
'': home,
'resumes/:id': 'showResume'
},
initialize: function (options) {
// layout is set in main.js
this.layout = options.layout
},
home: function () {
this.layout.render(new ResumeList({collection: resumes}));
},
showResume: function (cv) {
this.layout.render(new ShowResume({model: cv}));
}
});
and finally the ShowResume view:
var ShowResume = Backbone.View.extend({
initialize: function (options) {
this.model = options.model;
this.template = _.template($('#full-resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
}
});
I didn't provide the template for this view because it is quite large, but the error is following: whenever I try to navigate to a link, a view tries to render, but returns me the following error: Uncaught TypeError: this.model.toJSON is not a function. I suspect that my showResume method in router is invalid, but I can't actually get how to make it work in right way.
You are passing the string id of the url 'resumes/:id' as the model of the view.
This should solve it.
showResume: function (id) {
this.layout.render(new ShowResume({
model: new Backbone.Model({
id: id,
profession: "teacher" // you can pass data like this
})
}));
}
But you should fetch the data in the controller and react accordingly in the view.
var AppRouter = Backbone.Router.extend({
routes: {
'*otherwise': 'home', // notice the catch all
'resumes/:id': 'showResume'
},
initialize: function(options) {
// layout is set in main.js
this.layout = options.layout
},
home: function() {
this.layout.render(new ResumeList({ collection: resumes }));
},
showResume: function(id) {
// lazily create the view and keep it
if (!this.showResume) {
this.showResume = new ShowResume({ model: new Backbone.Model() });
}
// use the view's model and fetch
this.showResume.model.set('id', id).fetch({
context: this,
success: function(){
this.layout.render(this.showResume);
}
})
}
});
Also, this.model = options.model; is unnecessary as Backbone automatically picks up model, collection, el, id, className, tagName, attributes and events, extending the view with them.
Not sure where exactly I am going wrong. I cant seem to get the templates to render the title attributes within my nested json. I was following a tutorial online that was sort of holding my hand through this but I hit a brick wall and cant figure out why it wont render. Any help would be greatly appreciated.
Here is my templates and html.
<script id="albumTemplate" type="text/template">
<p><%= title %></p>
</script>
<script id="subalbumTemplate" type="text/template">
<p><%= title %></p>
</script>
<div class="row">
<div class="six columns"><h5>Albums</h5></div>
<div class="six columns"><h5>Sub-Albums</h5></div>
</div>
<div class="row" style="color: #333;">
<div class="six columns" id="categories"></div>
<div class="six columns" id="sub-cat"></div>
</div>
Here is my app.js
app.Subalbum = Backbone.Model.extend({
initialize: function () {
this.subId = this.get('id');
this.subTitle = this.get('title');
this.subImg = this.get('image');
this.subCanvas = this.get('canvas');
this.subSize = this.get('size');
}
});
app.Subalbums = Backbone.Collection.extend({
model: app.Subalbum
});
app.Album = Backbone.Model.extend({
initialize: function () {
this.subs = new app.Subalbums(this.get('subalbum'));
this.subs.parent = this;
this.albumId = this.get('id');
this.albumTitle = this.get('title');
this.albumImg = this.get('image');
}
});
app.Albums = Backbone.Collection.extend({
model: app.Album,
url: 'albums.json',
parse: function (data) {
return data;
}
});
app.AlbumCollectionView = Backbone.View.extend({
el: $("#categories"),
initialize: function () {
_.bindAll(this, 'render');
this.model.on('reset', function () {
this.render();
}, this);
},
render: function (event) {
_.each(this.model.models, function (album) {
//console.log(album.subs);
$(this.el).append(new app.AlbumView({
model: album
}).render().el);
}, this);
return this;
}
});
app.AlbumView = Backbone.View.extend({
template: _.template($("#albumTemplate").html()),
initialize: function () {
_.bindAll(this, 'render');
// Subalbum View should be instantiated and called from inside the initialize function of the Parent View
this.subView = new app.SubalbumView({
model: this.model.subs
});
this.subView.parentView = this; // this assignment connects the child view to the parent view
$("#sub-cat").append(this.subView.render().el); // subView should "return this" from child render() function
},
render: function () {
//console.log(this.model.subs);
//$(this.el).html("<p>" + this.model.get("title") + "</p>");
$(this.el).append(this.template(this.model.toJSON()));
return this;
}
});
app.SubalbumView = Backbone.View.extend({
template: _.template($("#subalbumTemplate").html()),
initialize: function () {
_.bindAll(this, 'render');
this.model.on('reset', function () {
this.render();
}, this);
},
render: function (event) {
_.each(this.model.models, function (subalbum) {
$(this.el).append("<p>" + subalbum.get("title") + "</p>");
//$(this.el).html(this.template(subalbum.toJSON()));
}, this);
return this;
}
});
app.AlbumRouter = Backbone.Router.extend({
routes: {
"": "indexRoute"
},
indexRoute: function () {
this.albumList = new app.Albums();
this.albumList.fetch();
this.albumAppView = new app.AlbumCollectionView({
model: this.albumList
});
}
});
var albumRoute = new app.AlbumRouter();
Backbone.history.start();
Here is the albums.json file structure.
[
{
"pid":0,
"title":"Blues Singer",
"image":"blues_singer.jpg",
"subalbum":[
{
"pid":0,
"title":"Another Realm",
"image":"another_realm.jpg"
},
{
"pid":1,
"title":"Ascendant",
"image":"ascendant.jpg"
},
{
"pid":2,
"title":"Ascent",
"image":"ascent.jpg"
}
]
},
{
"pid":1,
"title":"Destiny",
"image":"destiny.jpg",
"subalbum":[
{
"pid":0,
"title":"Cathedral of Trees",
"image":"cathedral_of_trees.jpg"
},
{
"pid":1,
"title":"Come Up Here",
"image":"come_up_here.jpg"
},
{
"pid":2,
"title":"Crystal Forest",
"image":"crystal_forest.jpg"
}
]
},
{
"pid":2,
"title":"Eagle",
"image":"eagle.jpg",
"subalbum":[
{
"pid":0,
"title":"Curved Road",
"image":"curved_road.jpg"
},
{
"pid":1,
"title":"Dawn Breaking",
"image":"dawn_breaking.jpg"
},
{
"pid":2,
"title":"Dawn",
"image":"dawn.jpg"
}
]
},
{
"pid":3,
"title":"Evening Harvest",
"image":"evening_harvest.jpg",
"subalbum":[
{
"pid":0,
"title":"Destiny",
"image":"destiny.jpg"
},
{
"pid":1,
"title":"Destiny2",
"image":"destiny2.jpg"
},
{
"pid":2,
"title":"Eagle Rising",
"image":"eagle_rising.jpg"
}
]
}
]
The problem is you wait for the reset event from this.albumList.fetch(), however the reset isn't triggered by default, so you need to do pass {reset:true} to fetch. Here is a JSFIDDLE.
indexRoute: function () {
this.albumList = new app.Albums();
// This view will render when the model's reset event is triggered.
// Since albumList is a collection, it should identified as such.
this.albumAppView = new app.AlbumCollectionView({
// albumList is a collection and it should identified as such.
// instead of model:this.albumList - now within albumAppView, you will have
// access to the collection via this.collection instead of this.model
collection: this.albumList
});
this.albumList.fetch({reset:true});
}
Some side suggestions (I'm going to pick on your AlbumCollectionView) but the same goes for your other views as well:
Instead of el: $("#categories") you can just use a string el:'#categories'
In initialize, You are using this.model.on when you could take advantage of listenTo. The main advantage of using listenTo is when call remove on a view, the event listeners are cleaned up for you.
// this works.
this.collection.on('reset', function () {
this.render();
}, this);
// can be written as (remember the we changed model to collection above).
this.listenTo(this.collection,'reset',this.render);
Moving onto your render function, a Backbone.Collection has a slew of underscore methods attached to them.
// while this works
_.each(this.collection.models, function (album) { ... });
// can be written as:
this.collection.each(function(album) { ... });
});
$(this.el), is sort of dated, you can use this.$el, which is just a cached jQuery object for the view's element. $el documentation.
So when we put it all together we wind up with:
app.AlbumCollectionView = Backbone.View.extend({
el: '#categories',
initialize: function () {
this.listenTo(this.collection,'reset', this.render);
},
render: function () {
this.collection.each(function (album) {
var albumView = new app.AlbumView({model: album});
this.$el.append(albumView.render().el);
}, this);
return this;
}
});
I want to clean up my views in Backbone, before loading new views.
For that, I have searched and added a prototype property to the Backbone object called close:
Backbone.View.prototype.close = function () {
this.$el.empty();
//this.$el.unbind();
this.undelegateEvents();
console.log('close');
};
P.S. the empty() does not seem to work, neither does unbind -- undelegateEvents works.
Now, I cannot seem to figure out hot to call the close function on my router:
define([
'jquery',
'underscore',
'backbone',
'views/userList',
'views/addUser'
], function ($, _, Backbone, UserList, AddUser) {
var Router = Backbone.Router.extend({
routes: {
'' : 'home',
'new' : 'addUser'
}
});
var initialize = function () {
var router = new Router();
router.on('route:home', function () {
userList = new UserList();
userList.render();
});
router.on('route:addUser', function () {
addUser = new AddUser();
addUser.render();
});
Backbone.history.start();
}
return {
initialize : initialize
}
});
Any ideas?
First of all, don't use close, just override the standard remove method:
var SomeBaseView = Backbone.View.extend({
remove: function () {
this.$el.empty(); // Instead of this.$el.remove();
this.stopListening();
this.undelegateEvents();
return this;
}
});
and then derive all your views from SomeBaseView. Messing around with Backbone.View.prototype should be avoided.
Then, stop using global variables like this:
userList = new UserList();
in your route handlers.
Then, start keeping track of which view is currently open so that you can call remove on it before throwing up the next view. A router more like this perhaps:
var Router = Backbone.Router.extend({
routes: {
'' : 'home',
'new' : 'addUser'
},
home: function() {
if(this.currentView)
this.currentView.remove();
this.currentView = UserList();
this.currentView.render();
},
addUser: function() {
if(this.currentView)
this.currentView.remove();
this.currentView = AddUser();
this.currentView.render();
}
});
You'd drop the router.on calls since you don't need them with this approach.
I am having difficulty with something very simple in Backbone. I want to wire up the <h1> in my page so that when the user clicks on it, it returns seamlessly to the homepage, without a postback.
This is the HTML:
<h1><a id="home" href="/">Home</a></h1>
(UPDATE: fixed ID as suggested by commenter.) And this is my Backbone view and router:
var HomeView = Backbone.View.extend({
initialize: function() {
console.log('initializing HomeView');
},
events: {
"click a#home": "goHome"
},
goHome: function(e) {
console.log('goHome');
e.preventDefault();
SearchApp.navigate("/");
}
});
var SearchApp = new (Backbone.Router.extend({
routes: {
"": "index",
},
initialize: function(){
console.log('initialize app');
this.HomeView = new HomeView();
},
index: function(){
// do stuff here
},
start: function(){
Backbone.history.start({pushState: true});
}
}));
$(document).ready(function() {
SearchApp.start();
});
The console is showing me
initialize app
initializing HomeView
But when I click on the <h1>, the page posts back - and I don't see goHome in the console.
What am I doing wrong? Clearly I can wire up the <h1> click event simply enough in jQuery, but I want to understand how I should be doing it in Backbone.
If you enable pushState you need to intercept all clicks and prevent the refresh:
$('a').click(function (e) {
e.preventDefault();
app.router.navigate(e.target.pathname, true);
});
Something like:
$(document).ready(function(){
var HomeView = Backbone.View.extend({
initialize: function() {
console.log('initializing HomeView');
}
});
var AboutView = Backbone.View.extend({
initialize: function() {
console.log('initializing AboutView');
}
});
var AppRouter = Backbone.Router.extend({
routes: {
"": "index",
"about":"aboutView"
},
events: function () {
$('a').click(function (e) {
e.preventDefault();
SearchApp.navigate(e.target.pathname, true);
});
},
initialize: function(){
console.log('initialize app');
this.events();
this.HomeView = new HomeView();
},
index: function(){
this.HomeView = new HomeView();
},
aboutView : function() {
this.AboutView = new AboutView();
}
});
var SearchApp = new AppRouter();
Backbone.history.start({pushState: true});
});
Your tag id is invalid, try this:
<h1><a id="home" href="/">Home</a></h1>