Cannot unbind and close events in my Backbone router - javascript

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.

Related

Model method error while trying to navigate

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.

Backbone: Best way to prevent routes (and url change)

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").

How to call a backbone view function from another view in separate files

I have two backbone views defined in two separate files namely:
LandingView.js
define([
'jquery',
'underscore',
'backbone',
'marionette',
'text!templates/landing/landingTemplate.html',
'text!templates/invitations/invitationsTemplate.html',
'views/invitations/InvitationsView',
], function ($, _, Backbone, Marionette, landingTemplate, invitationsTemplate, InvitationsView) {
var LandingView = Backbone.View.extend({
el : $("#landing"),
id : 'landing',
transition : 'slide',
initialize : function () {
this.GetNotificationsCounts();
},
events : {
'click #invitations' : 'onInvitations',
},
render : function () {
var that = this;
$('.menu li').removeClass('active');
$('.menu li a[href="#"]').parent().addClass('active');
this.$el.html(landingTemplate);
},
cleanup: function() {
this.undelegateEvents();
$(this.el).empty();
},
onInvitations : function () {
//do something
},
GetProfile: function (userLogin) {
// do something
},
GetNotificationsCounts: function () {
// do something
},
formatAccountName: function () {
//do something
}
});
return LandingView; });
Then there is another file InvitationsView.js
define([
'jquery',
'underscore',
'backbone',
'marionette',
'views/landing/LandingView',
'text!templates/invitations/invitationsTemplate.html',
], function ($, _, Backbone, Marionette, LandingView, invitationsTemplate ) {
var InvitationsView = Backbone.View.extend({
el : $("#invitations"),
id : 'invitations',
transition : 'slide',
initialize : function () { debugger;
this.$el.attr('data-transition', this.transition);
this.currentUserLogin = currentUserLogin;
var that = this;
},
events : {
},
render : function () { debugger;
$('.menu li').removeClass('active');
$('.menu li a[href="#"]').parent().addClass('active');
this.GetUserInvitationDetails();
this.$el.html(invitationsTemplate);
},
cleanup: function() {
this.undelegateEvents();
$(this.el).empty();
},
GetUserInvitationDetails: function () {
var landingView = new LandingView();
currentUserName= landingView.formatAccountName();
curUser = currentUserName.replace("\\", "^").replace("^", "%5E");
var profilemsg = landingView.GetProfile(currentUserName);
},
});
return InvitationsView;});
Now I need to call the formatAccountName and GetProfile functions defined in the first JS to the second JS. I am unable to do that. I get errors.
When I try
var landingView = new LandingView();
currentUserName= landingView.formatAccountName();
This also fails. Can somebody help me in this regard and tell me how can I achieve this
Your current approach of calling the formatAccountName method works. The following jsfiddle shows this:
http://jsfiddle.net/v4h11qac/
The problem is likely caused by another error that has not been handled correctly, resulting in the code not being run. You should fix the existing errors and the method call should work as expected.
Orignal Answer:
You could call the method directly on the prototype object:
LandingView.prototype.formatAccountName();
If you need to pass through a new context you can use the call or apply method as below:
LandingView.prototype.formatAccountName.call(context);
A better approach might involve creating a helper module that can be shared by both views.
var viewHelpers = {
formatAccountName: function(account) {
// ...
}
};
var ViewOne = Backbone.View.extend({
render: function() {
var formattedName = viewHelpers.formatAccountName(this.model);
// ...
}
};
var ViewTwo = Backbone.View.extend({
render: function() {
var formattedName = viewHelpers.formatAccountName(this.model);
// ...
}
};
You could also use a system bus, however that may be a little too heavy for such a use case. If you want to take a look at that path, then Backbone.Radio provides a request/response pattern which could be used to fulfill this requirement.
You could use a global event dispatcher that you declare in some kind of main-js-file like this:
var vent = _.extend({}, Backbone.Events);
Then in your view, listen for an event and run function.
vent.on('your:event',this.function_to_run_from_other_view,this);
Dispatch the event like this from the other view.
vent.trigger('your:event',args)

Passing control back to a Backbone Router from a view?

I have a loginview and its loginmodel. loginmodel is created from the initialize of the mainrouter. this way according to the loginmodel's loginp attribute mainrouter's render_home decides whether to render homeview or navigate to login route which calls render_login. render_login have a method to save() the loginmodel populated from the loginview's input fields. So if the data is correct save() responded and loginmodel changes, its loginp attribute becomes true. so now the homeview should be rendered. but I cannot success this.
I think at this point the control should be passed back to the mainrouter's route that invokes render_home above again. So it tests loginView.model.get('loginp') == true and results in true so this time creates homeview and renders it via homeView.render() defined in the homeview already.
I put a listenTo in the loginview for when its model changes or syncs to call route in mainrouter to call render_home. but this ends in endless back and forths between these two view without any render. this.listenTo( this.loginView.model, 'sync', Backbone.history.navigate("",{trigger:true}));
route "" is assigned to render_home by the way.
What am I missing. Is it not possible to pass control back to a router?
main router:
define([
'jquery',
'ratchet',
'underscore',
'backbone',
'login/loginmodel',
'login/loginview',
'register/registerview',
'home/homeview',
],
function($, Ratchet, _, Backbone, LoginModel, LoginView, RegisterView, HomeView){
var MainRouter = Backbone.Router.extend({
routes: {
"": "render_home",
"login": "render_login",
"register": "render_register"
},
initialize: function(){
this.loginView = new LoginView;
},
render_home: function(){
this.loginView.model.fetch({
success: function(model){
if( model.get('loginp') == true ){ //show you app view for logged in users!
this.homeView = new HomeView();
this.homeView.render();
}
else { //not logged in!
Backbone.history.navigate("/login", {trigger:true})
}
},
});
},
render_login: function(){ //display your login view
this.loginView.render();
},
render_register: function(){ //display your register view
this.registerView = new RegisterView;
this.registerView.render();
},
});
return MainRouter;
});
loginview:
define([
'jquery',
'ratchet',
'underscore',
'backbone',
'login/loginmodel',
'text!login/logintemplate.html',
],
function($, Ratchet, _, Backbone, LoginModel, LoginTemplate){
var LoginView = Backbone.View.extend({
el: $('body'),
initialize: function(){
this.model = new LoginModel;
},
template: _.template( LoginTemplate ),
render: function(){ //display your login view
this.$el.html( this.template( this.model.attributes ) );
},
events: {
'keyup input' : 'updateform',
'click #loginbutton' : 'login',
'click #renderregisterbutton' : 'render_register',
},
updateform: function(e){
var el = e.target;
var formData = {};
formData[ el.id ] = $(el).val();
this.model.set( formData );
},
login: function(e){
e.preventDefault();
this.model.save();
console.log( JSON.stringify( this.model ) );
},
render_register: function(){
Backbone.history.navigate("/register", {trigger:true});
},
});
return LoginView;
});
loginmodel:
define([
'underscore',
'backbone',
],
function(_, Backbone) {
var LoginModel = Backbone.Model.extend({
urlRoot: '/log_in',
defaults: {
username: null,
},
});
return LoginModel;
});
You absolutely can "pass control" from a Route to a View and back; your render_login function does exactly that:
render_login: function(){ //display your login view
// 1) router has control
this.loginView.render(); // 2) control passes to loginView
// 3) control passes back to the router
},
But that only works with synchronous logic. I think the problem you're having is because you are using asynchronous logic (this.loginView.model.fetch()) inside your render_home method. Asynchronous logic splits off from the main execution path, so the path for render_home is more like:
// 0) Router has control, and calls render_home
render_home: function(){
// 1) LoginView has control
this.loginView.model.fetch({
success: function(model){
// 4) LoginView has control once again, but now routing is done
if( model.get('loginp') == true ){ //show you app view for logged in users!
this.homeView = new HomeView();
this.homeView.render();
}
else { //not logged in!
Backbone.history.navigate("/login", {trigger:true})
}
},
});
// 2) LoginView still has control
},
// 3) Control passes back to the Router, which finishes routing
So, by the time the fetch returns, there is no routing logic to come back to.
However, this is easy enough to fix: just call Backbone.history.navigate (with trigger: true) from your fetch success handler. This will start a new routing process, and by that point (since the fetch has completed) the LoginModel will now be populated and your routing logic should work.

How to trigger an Event after rendering Views with Backbone.js?

I have many Views whos work with templates. The Rendering with the Views work perfectly, now into my Router i'm seeking to a way to trigger an Event when all Views rendered!
I used js loader like LAB.js but nothing work!
After all rendered i enter the event into the firebug console and it's work!
How and Here can i place my event so that it's trigger when all views rendered!
**My Event : **
$('div[id ^="solfg_"]').mobilyblocks();
**Router : **
(function () {
window.AppRouter = Backbone.Router.extend({
routes: {
"" : "init"
},
init: function(){
this.solfsmodel = new Solfs();
this.solfsmodel.fetch();
this.solfsView = new SolfsView({model: this.solfsmodel});
}
});
var app_router = new AppRouter;
Backbone.history.start();
}(jQuery));
Thank you
update : same problems
I found the solution just use $.when().then() from jquery, really amazing that i never saw this jquery function.
*My Solution : *
(function () {
window.AppRouter = Backbone.Router.extend({
routes: {
"" : "run"
},
initialize: function(){
this.solfsmodel = new Solfs();
this.solfsView = new SolfsView({model: this.solfsmodel});
},
run: function(){
$.when(
this.solfsmodel.fetch();
).then(function(){
*$('div[id ^="solfg_"]').mobilyblocks();*
});
}
});
var app_router = new AppRouter;
Backbone.history.start();
}(jQuery));
You can use the success callback of fetch method if you just need to wait that the collection is fetched (source: http://backbonejs.org/#Collection-fetch).
It's preferable to use Backbone.js method before use other librairies.
So your code should look like:
(function () {
window.AppRouter = Backbone.Router.extend({
routes: {
"" : "run"
},
initialize: function(){
this.solfsmodel = new Solfs();
this.solfsView = new SolfsView({model: this.solfsmodel});
},
run: function(){
this.solfsmodel.fetch({
success: function () {
$('div[id ^="solfg_"]').mobilyblocks();
}
);
}
});
var app_router = new AppRouter;
Backbone.history.start();
}(jQuery));

Categories