Where is this event calling to in MarionetteJS - backbone.js

I am exploring the BBCloneMail demo application for MarionetteJS, but I am not seeing how the events are triggering the rendering actions. I saw some global 'show' event here:
https://github.com/marionettejs/bbclonemail/blob/master/public/javascripts/bbclonemail/components/appController.js#L25
show: function(){
this._showAppSelector("mail");
Marionette.triggerMethod.call(this, "show");
},
But I don't see, where/how the Marionette.triggerMethod results into rendering the Mail component. I was trying to call the triggerMethod for my case, but I get a 'cannot call apply for undefined'. Why is the call above working for the BBcloneMail application.
The Application controller for my case:
MA.AppController = Marionette.Controller.extend({
initialize: function(){
_.bindAll(this, "_showGenres");
},
show: function() {
if (MA.currentUser) {
MA.navbar.show(new MA.Views.Items.LogoutNavbar({model: MA.currentUser}));
}
else
{
MA.navbar.show(new MA.Views.Items.LoginNavbar());
}
this._showGenres();
},
_showGenres: function() {
var categoryNav = new MA.Navigation.Filter({
region: MA.filter
});
this.listenTo(categoryNav, "genre:selected", this._categorySelected);
categoryNav.show();
MA.main.show(MA.composites.movies);
},
showMovieByGenre: function(genre){
var movies = new MA.Controllers.MoviesLib();
that = this;
$.when(movies.getByCategory(genre)).then(that._showMovieList);
Backbone.history.navigate("#movies/genres/" + genre);
},
_showMovieList: function(movieList){
var moviesLib = new MA.Controllers.MoviesLib({
region: MA.main,
movies: movieList
});
Marionette.triggerMethod.call(this, "show");
}
});
I init the application controller in a init.js with:
app = new MA.AppController();

Looking at the source for triggerMethod, this is a way of both triggering an event (the string being passed in), and additionally (if it exists) running a method on the object that has an 'on' prefix.
In your case the error relates to line 560, specifically that there is no method apply on undefined. Based on the code its (in your case) trying to call the equivilent of this.trigger('show') - but AppController doesn't have a method called trigger.
In which case I'm guessing that in the BBCloneMail example this (being bassed into triggerMethod.call) is not actually the controller, but instead the view that is to be shown.

Related

How to test if an event handler is set in a Marionette.js view?

There is a Marionette.js view which acts as a login form. The following code sample shows the relevant code parts including an error I already fixed:
MyApp.module("User", function(User, App, Backbone, Marionette, $, _) {
User.LoginView = Marionette.ItemView.extend({
className: "reveal-modal",
template: "user/login",
ui: {
signInForm: "#signin-form"
},
events: {
"submit #signin-form": "onSignInFormSubmit"
},
onRender: function() {
var self = this;
var $el = this.$el;
// [...] Render schema
_.defer(function(){
$el.reveal({
closeOnBackgroundClick: false,
closed: function(){
self.close(); // <-- This is incorrect. Do not close the ItemView directly!
}
});
});
},
onSignInFormSubmit: function(event) {
event.preventDefault();
var errors = this.signInForm.validate();
var data = this.signInForm.getValue();
// [...] Notify that data has been submitted.
},
hideForm: function() {
this.$el.trigger("reveal:close");
}
});
});
I noticed a major mistake in my implementation. In the callback function closed of Reveal I decided to close the ItemView directly which is wrong as you can read in the documentation of Marionette.js:
View implements a close method, which is called by the region managers automatically.
Bugfix: Instead close() should be called on the region. I fixed this error.
Now I ask myself how can I actually write a test which covers the problem. I use Jasmine for testing. I noticed that the event handler onSignInFormSubmit is no longer called after I have incorrectly closed the ItemView and try to re-submit the form.
Here is a first draft of the test which unfortunately does fail also with the bugfix:
it("should call the submit handler for the sign-in form", function() {
spyOn(userController.loginView, "onSignInFormSubmit");
spyOn(userController.loginView.signInForm, "validate").andCallFake(function(params) {
return null;
});
userController.loginView.hideForm();
userController.loginView.ui.signInForm.trigger("submit");
expect(userController.loginView.onSignInFormSubmit).toHaveBeenCalled();
});
Maybe one could also test if the event handler is registered such as:
expect(userController.loginView.events["submit #signin-form"]).toEqual("onSignInFormSubmit");

How do I simply refresh a view in Backbone?

I'm brand new to backbone and just learning the basics. I am building an image gallery with backbone. I am displaying a large version of an image. The routing is working properly. When a url is passed with an id the appropriate JSON is loaded into the model and the html is injected into the dom. Everything displays as expected.
However, I tried entering a url for the JSON for an image that didn't exist and noticed that the view still rendered but with the previously rendered view's properties (image url) still present. How do I ensure that the view is refreshed - all empty properties? Or is it the model that needs to be refreshed?
Note: I am re-using the view to avoid the overhead of creating and dystroying the view itself.
Here is the view in question:
var ImageView = Backbone.View.extend({
template: Handlebars.compile(
'<div class="galleryImageSingle">'+
'<h2>{{title}}</h2>' +
'<img id="image" src="{{imageUrl}}" class="img-polaroid" />' +
'<div class="fb-share share-btn small"><img src="img/fb-share-btn- small.png"/></div>'+
'</div>' +
'<div class="black-overlay"></div>'
),
initialize: function () {
this.listenTo(this.model, "change", this.render);
//this.model.on('change',this.render,this);
},
fbSharePhoto: function () {
console.log('share to fb ' + this.model.attributes.shareUrl)
},
close: function () {
//this.undelegateEvents();
this.remove();
},
render: function () {
this.$el.html(this.template(this.model.attributes));
this.delegateEvents({
'click .fb-share' : 'fbSharePhoto',
'click .black-overlay' : 'close'
});
return this;
}
})
Here is the router:
var AppRouter = Backbone.Router.extend({
routes: {
"" : "dashboard",
"image/:iId" : "showImage",
},
initialize: function () {
// this.galleriesCollection = new GalleriesCollection(); //A collection of galleries
// this.galleriesCollection.fetch();
this.imageModel = new Image();
this.imageView = new ImageView ({ model: this.imageModel });
},
dashboard: function () {
console.log('#AppRouter show dashboard - hide everything else');
//$('#app').html(this.menuView.render().el);
},
showImage: function (iId) {
console.log('#AppRouter showPhoto() ' + iId);
this.imageModel.set('id', iId);
this.imageModel.fetch();
$('#imageViewer').html(this.imageView.render().el);
}
});
Is is it that the model still has the old info or the view, or both?
For extra credit, how could I detect a failure to fetch and respond to it by not triggering the corresponding view? Or I am I coming at it wrongly?
Thanks in advance for any advice.
////////////////////////////////////////////////////////////////////////////
Looks like I found something that works. I think just the process of framing the question properly helps to answer it. (I'm not allowed to answer the question so I'll post what I found here)
It appears that its the model that needs refreshing in this case. In the app router when I call the showImage function I clear the model and reset its values to default before calling fetch and this did the trick. Ironically the trick here is showing a broken image tag.
showImage: function (iId) {
console.log('#AppRouter showPhoto() ' + iId);
this.imageModel.clear().set(this.imageModel.defaults);
this.imageModel.set('id', iId);
this.imageModel.fetch();
$('#imageViewer').html(this.imageView.render().el);
}
For my own extra credit offer: In the event of an error (if needed fetch() accepts success and error callbacks in the options hash). Still definitely open to hearing about a way of doing this thats baked in to the framework.
You can just update the model like this:
ImageView.model.set(attributes)

How to retrieve an object from Parse.com after saving it

I'm creating an application for Phonegap using Backbone framework and Parse.com as backend service. I create an object with Parse.com (corresponding to Backbone models).
This object has a saveDraftToP() method that calls the Parse.com function save().
After this method is called from a view, I'd like to retrieve the updated object.
To do so I'm binding the 'change' event to the model but the Parse assigned ID is undefined.
Here is the code of the model:
var Match = Parse.Object.extend("Match", {
states: {'DRAFT': 0, 'RUNNING': 1, 'ENDED': 2},
saveDraftToP: function () {
var self = this;
this.save({
user: Parse.User.current(),
ACL: new Parse.ACL(Parse.User.current()),
state: self.states.DRAFT
}, {
success: function (result) {
self = result;
},
error: function (e) {
}
});
}
});`
And here is the code for the view:
var vmNuovaPartita = Parse.View.extend({
template: Handlebars.compile(template),
model: new Match(),
collection: new HintCollection(),
initialize: function () {
this.bind("change:model", console.log(this.model.id) , this);
},
render: function (eventName) {
var match = this.model.toJSON();
$(this.el).html(this.template(match));
return this;
}
});
I'm not quite sure why you have a save function wrapped in another save-like function. :-)
Say you have something like myMatch which is an object.
Through your UI, a button click saves the object data. You can just use myMatch.save({attr:val, ...}) straight out of the box. Backbone (and Parse) by default are optimistic. That means, you it will set the values of the model with the expectation that persisting to the server will succeed.
Thus, you don't need to retrieve anything extra. You already have the model in it's most current state.
To have a model view that responds to these changes, I'd design the view a little differently.
var vmNuovaPartita = Parse.View.extend({
template: Handlebars.compile(template),
initialize: function () {
this.model.on('change', this.render);
},
render: function (eventName) {
var match = this.model.toJSON();
$(this.el).html(this.template(match));
return this;
}
});
var myView = new vmNuovaPartita({
model: myModel
});
I'd initialize the model outside of the view, then pass it in as an option when you generate a new view. When you pass a model in as an option, it's special and will be attached directly to the view ... view.model which you can refer inside your view code as this.model
In the init we place a listener on the model for change events, then fire off a rerender of the view. Or a nicer way to go about this sort of thing is to throw in the newer Backbone Events with the .listenTo() method.

Backbone.js MVC way to render the view AFTER the data is received back from the server on a fetch?

I wish to read a whole database table to fill a Backbone.js Collection, before updating a View.
I am using fetch and listening to the reset event.
My problem is the reset event fires up before the http request is made to the server.
My question is: how can I render the view AFTER the data is received back from the server on a fetch?
Here is a jsfiddle showing the problem (with a debugger placed at reset):
http://jsfiddle.net/GhaPF/16/
The code:
$(document).ready(function() {
var Item = Backbone.Model.extend({
urlRoot : './items'
});
var ItemList = Backbone.Collection.extend({
model: Item,
url: './items/',
});
var ItemListView = Backbone.View.extend({
el: 'body',
initialize: function(myitemList) {
this.itemlist = myitemList;
this.itemlist.bind('reset', this.debuggThis());
},
debuggThis: function() {
debugger;
},
render: function() {
},
events: {
"keypress #new-item": "createOnEnter"
},
createOnEnter: function(e) {
}
});
$("#new-item").focus();
var itemlist = new ItemList();
var myitemListView = new ItemListView(itemlist);
itemlist.fetch();
});​
The following code works, but it just doesn't feel like proper backbone.js (MVC) code since it would be placed outside of the View definition:
itemlist.fetch().complete(function(){
Maybe the issue is this line:
this.itemlist.bind('reset', this.debuggThis());
Should actually be:
this.itemlist.bind('reset', this.debuggThis);
Your debugThis function was getting run at the time you set up the listener for the 'reset' event - not when the event is triggered. This was telling JavaScript that you wanted debugThis to return a callback function instead of having debugThis "be" the callback function.
Also, orangewarp's comment about passing 'this' as the third parameter is probably relevant too. Sot it would end up as:
this.itemlist.bind('reset', this.debuggThis, this);
That's strange. When you fetch() the reset event should be triggered AFTER your collection is populated. So I'm thinking the phenomena that reset happens before the http request is fired up may not be what you think it is.
Instead of using the complete... you could always just use the success callback option like this:
itemlist.fetch({
success: function() {
// Whatever code you want to run.
itemlist.debuggThis();
}
});
Also, when binding your reset you probably want this:
this.itemlist.bind('reset', this.debuggThis, this);

Backbone.js:"Maximum call stack size exceeded" error

suppose I have a model and a view ,ths view have two method:one is bind the document mousemove event and the other is unbind method,defalut I give the document mousemove event, once the model's enable value changed I will call the view's unbind method:
window.ConfigModel = Backbone.Model.extend({
defaults: {
'enable':0
},
initialize: function(){
this.bind("change:enable", function () {
var portView2 = new PortView();
portView2.viewOff();
});
},
change:function () {
this.set('enable', 9);
}
})
window.PortView = Backbone.View.extend({
viewOn: function () {
$(document).on('mousemove', function () {
console.log('move')
})
},
viewOff: function () {
$(document).off('mousemove');
}
})
then I put an input on the document to call the model changed:
$('input').click(function () {
var configModel = new ConfigModel();
configModel.change();
})
the boot script is :
var portView1 = new PortView();
portView1.viewOn();
The problem is once I call the click the input button ,the chrome would tell me an error:Maximum call stack size exceeded it seems the change be invoke many times.So what's the problem with my problem ,how can I solve this problem
Backbone models already have a change method:
change model.change()
Manually trigger the "change" event and a "change:attribute" event for each attribute that has changed. If you've been passing {silent: true} to the set function in order to aggregate rapid changes to a model, you'll want to call model.change() when you're all finished.
Presumably something inside Backbone is trying to call configModel.change() and getting your version of change which triggers another change() call inside Backbone which runs your change which ... until the stack blows up.
You should use a different name for your change method.
That said, your code structure is somewhat bizarre. A model listening to events on itself is well and good but a model creating a view is odd:
initialize: function() {
this.bind("change:enable", function () {
var portView2 = new PortView();
portView2.viewOff();
});
}
And instantiating a view simply to call a single method and then throw it away is strange as is creating a new model just to trigger an event.
I think you probably want to have a single ConfigModel instance as part of your application state, say app.config. Then your click handler would talk to that model:
$('input').click(function () {
app.config.enable_level_9(); // or whatever your 'change' gets renamed to
});
Then you'd have some other part of your application (not necessarily a view) that listens for changes to app.config and acts appropriately:
app.viewOn = function() {
$(document).on('mousemove', function() {
console.log('move')
});
};
app.viewOff = function() {
$(document).off('mousemove');
};
app.init = function() {
app.config = new ConfigModel();
app.viewOn();
$('input').click(function () {
app.config.enable_level_9();
});
// ...
};
And then start the application with a single app.init() call:
$(function() {
app.init();
});

Resources