Strange behaviour of this in backbone.js Controller - backbone.js

Yes I am new to JS and also in backbonejs.
Lets dig into the problem now.
I am having a very strange behaviour of this in backbonejs Controller.
Here is the code of my controller
var controller = Backbone.Controller.extend( {
_index: null,
routes: {
"": "index"
},
initialize: function(options){
var self = this;
if (this._index === null){
$.getJSON('data/album1.json',function(data) {
//this line is working self._index is being set
self._index = new sphinx.views.IndexView({model: self._photos});
});
Backbone.history.loadUrl();
}
},
index: function() {
//this line is not working
//here THIS is pointing to a different object
//rather than it was available through THIS in initialize method
this._index.render();
}
});
Here is the lines at the end of the file to initiate controller.
removeFallbacks();
gallery = new controller;
Backbone.history.start();
Now , i am missing something. But what ???
If this is the wrong way what is the right way??
I need to access the properties i set from the initialize method from index method.
It looks like the caller function of index method is changing it's scope.
I need to preserve the scope of that.

You have to specify the route action into a Backbone Route not into a Controller. Inside the router is where you are going to initialize your controller and views.
Also, there is no method Backbone.history.loadURL(). I think you should use instead Backbone.history.start() and then call the navigate in the router instance e.g. router.navigate('state or URL');
var myApp = Backbone.Router.extend( {
_index: null,
routes: {
"": "index"
},
initialize: function(options){
//Initialize your app here
this.myApp = new myApp();
//Initialize your views here
this.myAppViews = new myAppView(/* args */);
var self = this;
if (this._index === null){
$.getJSON('data/album1.json',function(data) {
//this line is working self._index is being set
self._index = new sphinx.views.IndexView({model: self._photos});
});
Backbone.history.loadUrl(); //Change this to Backbone.history.start();
}
},
// Route actions
index: function() {
this._index.render();
}
});

Related

Backbone is interpolating query string in model URL. How do I stop it?

I have a Backbone model that looks like this
...
var Address = Backbone.Model.extend({
urlRoot: '/address/'
});
return { address: Address }
});
I have a template that prints out an address in a form. The template is rendered by a view that is passed an address id in it's 'render' function. The view is reached by a route like this 'address/:id'.
The view looks like this:
var AddressView = Backbone.View.extend({
el: $('#myclass'),
render: function(options) {
var that = this;
var addr = new A.address({id: options.aid});
addr.fetch({
reset: true,
success: function(address) {
var template = _.template(ATemplate, {address: address});
that.$el.html(template);
}
});
return this;
},
events: {
'submit .edit-address-form': 'editAddress'
},
editAddress: function(ev) {
//serializeObject creates JSON object from form data
var addressDetails = $(ev.currentTarget).serializeObject();
addr.save(addressDetails, function(addr) {
R.router.navigate('', {trigger: true});
});
return false;
}
});
return {
addressView: new AddressView()
};
});
There are two problems. The first problem is that the 'editAddress' function is never getting called, even though the class name is correct and the button type = is 'submit'.
The second problem is when I submit the address form the URL is garbled, a query string is interpolated between the base URL and /#/address, as in
http:///ldmanclient/address=2500+Moffitt+Library&address2=University+of+California%2C+Berkeley&city=Berkeley&zipcode=94720&mailcode=6000&aid=1#/address/1
Has anyone seen this type of behavior before? What am I doing wrong?
As mu said, the form is being submitted the standard way before Backbone gets to it. Try preventing the submit action:
editAddress: function(ev) {
ev.preventDefault();
// same code as above
}

Backbone/Underscore _.each(...) doesn't seem to work according to docs

In this code...
_.each(this.photos, function(element,index,list) {
console.log('element...');
console.log(element);
var photoView = new PhotoView({photo:element});
self.$el.append(photoView.render());
});
element is the entire this.photos collection. Why is not just one photo element of the 10 in the collection?
EDIT: Here is my method that populates the photos collection....
loadPhotos: function(memberId) {
var self = this;
this.photos = new PhotosCollection([]);
this.photos.on('error', this.eventSyncError, this);
this.photos.fetch({
url: this.photos.urlByMember + memberId,
success: function(collection,response,options) {
console.log('Fetch photos success!');
self.render();
}
});
},
The collection loads with models just fine. In the Chrome console, I can see the collection of models. I'm not sure what's wrong. I cannot iterate the collection with any of the methods recommended by posters below.
You are using the _.each method incorrectly. The underscore methods needs to called directly on the collection:
this.photos.each(function(element,index,list) {
console.log('element...');
console.log(element);
var photoView = new PhotoView({photo:element});
self.$el.append(photoView.render());
});
Or you if want to use the _.each from you need to pass in the models property and not the collection object itself as the list:
_.each(this.photos.models, function(element,index,list) {
console.log('element...');
console.log(element);
var photoView = new PhotoView({photo:element});
self.$el.append(photoView.render());
});
One should use this.photos.each(function(elt, index, list){...}) instead of _.each(this.photos,...) because this.photos is not an underscorejs _.chain object.
Thank you for your suggestions! I would never have figured this out without all your advice above. So here was the problem...
In the parent view, this loads up photo records for a particular member...
loadPhotos: function(memberId) {
var self = this;
this.photos = new PhotosCollection([]);
this.photos.on('error',this.eventSyncError,this);
this.photos.fetch({
url: this.photos.urlByMember + memberId,
success: function(collection,response,options) {
self.render();
}
});
},
Still in the parent view, Backbone.Subviews uses this to call each child view when it renders. Note how I'm passing this.photos to the subvw-photos...
subviewCreators: {
"subvw-profile": function() {
var options = {member: this.member};
// do any logic required to create initialization options, etc.,
// then instantiate and return new subview object
return new ProfileView( options );
},
"subvw-photos": function() {
var options = {photos: this.photos};
return new PhotosView( options );
},
"subvw-comments": function() {
var options = {};
return new CommentsView( options );
}
},
This is in the subvw-photos child view. Note how the intialize is accepting the collection as a parameter. See this problem?...
initialize: function(photos) {
Backbone.Courier.add(this);
this.photos = photos;
},
render: function() {
console.log('rendering photosview now...');
var self = this;
this.photos.each(function(element,index,list) {
var photoView = new PhotoView({photo:element});
$(self.el).append(photoView.render());
});
return this;
},
I was passing an object wrapping the photos collection in to initalize but then treating it like it was just a ref to the photos collection. I had to change the subvw-photos initialize to the following...
initialize: function(args) {
Backbone.Courier.add(this);
this.photos = args.photos;
},
Then of course all the other code magically began working :-/
Thank you again for your tips! You definitely kept me on track :-)

Backbone Router -- load data

EDIT: Got it working, but it seems wrong.
I ending up adding a listener to the sync event on the main app view, then render a player. I also added a global variable PgaPlayersApp.CurrentPlayer.
Am I going about this the wrong way? What is the correct way to do this? Is there a reason I can't use reset: true and then listen for the reset event? (It doesn't work)
ROUTER:
// js/router.js
var PgaPlayersApp = PgaPlayersApp || {};
var Router = Backbone.Router.extend({
routes:{
'player/:id': 'loadPlayer'
},
loadPlayer: function(id)
{
PgaPlayersApp.CurrentPlayer.set('id', id);
PgaPlayersApp.CurrentPlayer.fetch();
}
});
PgaPlayersApp.Router = new Router();
Backbone.history.start();
VIEW:
//js/views/app.js
var PgaPlayersApp = PgaPlayersApp || {};
PgaPlayersApp.AppView = Backbone.View.extend({
el: '#pga_players_profile_app',
initialize: function()
{
this.listenTo(PgaPlayersApp.Players, 'reset', this.addAll);
this.listenTo(PgaPlayersApp.CurrentPlayer, 'sync', this.loadPlayer);
PgaPlayersApp.Players.fetch({reset: true});
},
...
loadPlayer: function()
{
new PgaPlayersApp.PlayerCardView({ model: PgaPlayersApp.CurrentPlayer }).render();
}
});
Backbone.js is a library that doesn't really enforce how you'd like to structure your App (Or the relationship between your Controller, Model, Router, etc.)
Below is one of the many ways to do it.
Couple highlights:
Router kicks off the fetch process.
When model has been fetched, Router then asks the View to render data (Instead of having the View listening to change events from the Model.)
This assumes that PlayerCardView is a "read only" view, as the View doesn't listen to change events from the Model. Depending on your use case, this might not be desirable, so it ultimately depends on how you'd like to handle it.
Here are some sample code:
(function (export) {
var App = export.App = {};
// Stores state/current views of the App
App.state = {};
App.state.currentPlayer = null;
// Model containing the player
App.PlayerModel = Backbone.Model.extend({});
// Single Player View (Assuming you have a collection view for list of players)
App.PlayerCardView = Backbone.View.extend({
model: App.PlayerModel,
template: _.template('<%= id %>'),
render: function(parentEl) {
// Render player
this.$el.html(this.template(this.model.toJSON()));
// Append player view to parent container
if (parentEl) {
parentEl.append(this.$el);
}
return this;
}
// Don't forget to clean up as well!
});
// Router
App.Router = Backbone.Router.extend({
routes: {
'player/:id': 'showPlayer'
},
showPlayer: function(id) {
// Unload current player view, if necessary
// Construct model
var player = App.state.currentPlayer = new App.Player({
id: id
});
// Pass model to the new player view
var view = App.state.currentPlayerView = new App.PlayerCardView({
model: App.state.currentPlayer
});
// At this time, you should probably show some loading indicator as you're fetching data from the server
// Fetch data
player.fetch({
success: function() {
// This would be called when data has been fetched from the server.
// Render player on screen
view.render($('#parentContainerId'));
}
});
}
});
// Initializes the App
App.init = function() {
// Kick off router
App.state.router = new App.Router();
export.Backbone.history.start();
};
})(window);
// Start the app!
window.App.init();
Gist: https://gist.github.com/dashk/5770073

Roles of backbone views, model, and router

I am developing a backbone application which is using require.js.
I want a user to enter in the 'id' for a model and then either be redirected to a view for that model if it exists, or display an error message if it does not. This sounds extremely simple, but I am having trouble figuring out the roles of each component.
In the application below, the user will come to an index page with an input (with id 'modelId') and a button (with class attribute 'lookup').
The following piece of code is the router.
define(['views/index', 'views/myModelView', 'models/myModel'],
function(IndexView, MyModelView, myModel) {
var MyRouter = Backbone.Router.extend({
currentView: null,
routes: {
"index": "index",
"view/:id": "view"
},
changeView: function(view) {
if(null != this.currentView) {
this.currentView.undelegateEvents();
}
this.currentView = view;
this.currentView.render();
},
index: function() {
this.changeView(new IndexView());
},
view: function(id) {
//OBTAIN MODEL HERE?
//var model
roter.changeView(new MyModelView(model))
}
});
return new MyRouter();
});
The following piece of code is the index view
define(['text!templates/index.html', 'models/myModel'],
function( indexTemplate, MyModel) {
var indexView = Backbone.View.extend({
el: $('#content'),
events: {
"click .lookup": "lookup"
},
render: function() {
this.$el.html(indexTemplate);
$("#error").hide();
},
lookup: function(){
var modelId = $("#modelId").val()
var model = new MyModel({id:modelId});
model.fetch({
success: function(){
window.location.hash = 'view/'+model.id;
},
error: function(){
$("#error").text('Cannot view model');
$("#error").slideDown();
}
});
},
});
return indexView
});
What I can't figure out is that it seems like the better option is for the index view to look up the model (so it can display an error message if the user asks for a model that doesn't exist, and also to keep the router cleaner). But the problem is that the router now has no reference to the model when the view/:id router is triggered. How is it supposed to get a hold of the model in the view() function?
I guess it could do another fetch, but that seems redundant and wrong. Or maybe there is supposed to be some global object that both the router and the view share (that the index view could put the model in), but that seems like tight coupling.
You can do something like this. You could do something similar with a collection instead of a model, but it seems like you don't want to fetch/show the whole collection?
With this type of solution (I think similar to what #mpm was suggesting), your app will handle browser refreshes, back/forward navigation properly. You basically have a MainView, which really acts more like a app controller. It handles events triggered either by the router, or by user interaction (clicking lookup or a back-to-index button on the item view).
Credit to Derick Bailey for a lot of these ideas.
In the Router. These are now only triggered if the user navigates by changing a URL or back/forward.
index: function() {
Backbone.trigger('show-lookup-view');
},
view: function(id) {
var model = new MyModel({id: id});
model.fetch({
success: function(){
Backbone.trigger('show-item-view', model);
},
error: function(){
// user could have typed in an invalid URL, do something here,
// or just make the ItemView handle an invalid model and show that view...
}
});
}
In new MainView, which you would create on app startup, not in router:
el: 'body',
initialize: function (options) {
this.router = options.router;
// listen for events, either from the router or some view.
this.listenTo(Backbone, 'show-lookup-view', this.showLookup);
this.listenTo(Backbone, 'show-item-view', this.showItem);
},
changeView: function(view) {
if(null != this.currentView) {
// remove() instead of undelegateEvents() here
this.currentView.remove();
}
this.currentView = view;
this.$el.html(view.render().el);
},
showLookup: function(){
var view = new IndexView();
this.changeView(view);
// note this does not trigger the route, only changes hash.
// this ensures your URL is right, and if it was already #index because
// this was triggered by the router, it has no effect.
this.router.navigate('index');
},
showItem: function(model){
var view = new ItemView({model: model});
this.changeView(view);
this.router.navigate('items/' + model.id);
}
Then in IndexView, you trigger the 'show-item-view' event with the already fetched model.
lookup: function(){
var modelId = $("#modelId").val()
var model = new MyModel({id:modelId});
model.fetch({
success: function(){
Backbone.trigger('show-item-view', model);
},
error: function(){
$("#error").text('Cannot view model');
$("#error").slideDown();
}
});
},
I don't think this is exactly perfect, but I hope it could point you in a good direction.

Backbone - Update URL from view function

I'm stumbling through my first backbone project, and am trying to update the URL after a button element is clicked.
I can manually set the URL via window.location.hash, but I assume this is not the correct way. Can anyone tell me what is the preferred way to update the URL?
var AppView = Backbone.View.extend({
events: {
"click #loadProject": "filterProject",
},
filterProject: function(){
var projectID = $('#selectProject option:selected').val();
var orgID = 'test';
// Is there a better way to do this?
window.location.hash = '/'+orgID+'/'+projectID;
this.renderProject(orgID,projectID);
},
renderProject:function(orgID,projectID){
//Some code
},
});
//Routing
var PropertiesRouter = Backbone.Router.extend({
routes: {
"/:who/:project":"getProjects", //#/org/1
"/:who":"getOrganisation", //#/org
"*actions": "defaultRoute"
},
getProjects:function(who,project){
app.renderProject(who,project);
},
getOrganisation:function(who){
app.renderOrganisation(who);
},
defaultRoute: function(actions){
app.renderHomePage();
},
});
var app = new AppView();
//create router instance
var propertiesRouter = new PropertiesRouter();
//start history service
Backbone.history.start();
Thanks!
You definitely can call window.location.hash, but this does break some of the separation backbone is trying to create for you. A better choice is to call router.navigate(..). See http://backbonejs.org/#Router-navigate

Resources