Rendering a closed Marionette view - backbone.js

Shouldn't a closed Marionette view re-delegate the defined events (events, modelEvents, CollectionEvents) when rendering again?
It seems as if I have to manually call delegateEvents after closing and re-rendering a view. Otherwise the view won't work as expected.
http://jsfiddle.net/4DCeY/
var app = new Marionette.Application();
app.addRegions({
main: '.main'
});
var MyView = Marionette.ItemView.extend({
template: _.template('Hi, I\'m a view! Foo is: <%= foo %>'),
modelEvents: {
'change': 'onChange'
},
onChange: function() {
alert('change!');
}
});
var Model = Backbone.Model.extend({});
app.addInitializer(function() {
var m = new Model({foo: 'bar'});
var myView = new MyView({
model: m
});
app.main.show(myView);
myView.close();
app.main.show(myView);
m.set({foo: 'baz'});
});
$(document).ready(function(){
app.start();
});

If I understand your question right, there are multiple open github issues about this.
For example:
https://github.com/marionettejs/backbone.marionette/pull/654
https://github.com/marionettejs/backbone.marionette/issues/622
Last time I checked, Derick (the creator of Marionette) didn't feel like reusing closed views should be something regions should do.
So you could
simply create a new view and show that one
manually call delegateEvents - but there was an issue with multiple event bindings that I can't remember right now, so be careful about that one (not at work right now, so can't take a peek at the code, sorry)
write your own region manager
or wait and see if Derick will merge one of the pull requests

a couple of points:
You do not need to call myView.close() Marionette Region will take care of that when you show another view
Marionette.Region will not replace the same view with itself. It will just skip the redundant procedure if you want to test this correctly you need 2 views
If you want a change in model to invoke render you must explicitly write it
I altered the jsfiddle with the following things:
added myView1 and myView2
removed explicit call to myView.close
added a call for this.render() from the onChange function
Here is the corrected jsfiddle http://jsfiddle.net/4DCeY/1/ :
app.addInitializer(function() {
var m = new Model({foo: 'bar'});
var myView1 = new MyView({
model: m
});
var myView2 = new MyView({
model: m
});
app.main.show(myView1);
app.main.show(myView2);
m.set({foo: 'baz'});
});
And:
onChange: function() {
alert('change!');
this.render();
}

Related

Bug while creating object in View

I'm working on a backbone.js project which is mainly to learn backbone framework itself.
However I'm stuck at this problem which i can't figure out but might have an idea about the problem...
I've got an Create View looking like this...
define(['backbone', 'underscore', 'jade!templates/addAccount', 'models/accountmodel', 'common/serializeObject'],
function(Backbone, underscore, template, AccountModel, SerializeObject){
return Backbone.View.extend({
//Templates
template: template,
//Constructor
initialize: function(){
this.accCollection = this.options.accCollection;
},
//Events
events: {
'submit .add-account-form': 'saveAccount'
},
//Event functions
saveAccount: function(ev){
ev.preventDefault();
//Using common/serializeObject function to get a JSON data object from form
var myObj = $(ev.currentTarget).serializeObject();
console.log("Saving!");
this.accCollection.create(new AccountModel(myObj), {
success: function(){
myObj = null;
this.close();
Backbone.history.navigate('accounts', {trigger:true});
},
error: function(){
//show 500?
}
});
},
//Display functions
render: function(){
$('.currentPage').html("<h3>Accounts <span class='glyphicon glyphicon-chevron-right'> </span> New Account</h3>");
//Render it in jade template
this.$el.html(this.template());
return this;
}
});
});
The problem is that for every single time I visit the create page and go to another and visit it again. It remebers it, it seems. And when i finally create a new account I get that many times I've visited total number of accounts...
So console.log("Saving!"); in saveAccount function is called x times visited page...
Do I have to close/delete current view when leaving it or what is this?
EDIT
Here's a part of the route where i init my view..
"account/new" : function(){
var accCollection = new AccountCollection();
this.nav(new CreateAccountView({el:'.content', accCollection:accCollection}));
console.log("new account");
},
/Regards
You have zombie views. Every time you do this:
new CreateAccountView({el:'.content', accCollection:accCollection})
you're attaching an event listener to .content but nothing seems to be detaching it. The usual approach is to call remove on a view to remove it from the DOM and tell it to clean up after itself. The default remove does things you don't want it to:
remove: function() {
this.$el.remove();
this.stopListening();
return this;
}
You don't want that this.$el.remove() call since your view is not responsible for creating its own el, you probably want:
remove: function() {
this.$el.empty(); // Remove the content we added.
this.undelegateEvents(); // Unbind your event handler.
this.stopListening();
return this;
}
Then your router can keep track of the currently open view and remove it before throwing up another one with things like this:
if(this.currentView)
this.currentView.remove();
this.currentView = new CreateAccountView({ ... });
this.nav(this.currentView);
While I'm here, your code will break as soon as you upgrade your Backbone. As of version 1.1:
Backbone Views no longer automatically attach options passed to the constructor as this.options, but you can do it yourself if you prefer.
So your initialize:
initialize: function(){
this.accCollection = this.options.accCollection;
},
won't work in 1.1+. However, some options are automatically copied:
constructor / initialize new View([options])
There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName, attributes and events.
so you could toss out your initialize, refer to this.collection instead of this.accCollection inside the view, and instantiate the view using:
new CreateAccountView({el: '.content', collection: accCollection})

BB Marionette: best way to update a collection without re-rendering

I have a very simple page that shows a collection in a table. Above it theres a search field where the user enters the first name of users.
When the user types I want to filter the list down.
Edit: I have updated the code to show how the current compositeView works. My aim is to integrate a searchView that can _.filter the collection and hopefully just update the collection table.
define([
'marionette',
'text!app/views/templates/user/list.html',
'app/collections/users',
'app/views/user/row'
],
function (Marionette, Template, Users, User) {
"use strict"
return Backbone.Marionette.CompositeView.extend({
template: Template,
itemView: User,
itemViewContainer: "tbody",
initialize: function() {
this.collection = new Users()
this.collection.fetch()
}
})
})
Divide your template in a few small templates, this increases performance at the client side, you don't have problems with overriden form elements and you have more reuseable code.
But be aware of too much separation, cause more templates means more views and more code/logic.
You don't seem to be making use of CollectionView as well as you could be. If I were you I would separate the concerns between the search box and the search results. Have them as separate views so that when one needs to rerender, it doesn't effect the other.
This code probably won't work straight away as I haven't tested it. But hopefully it gives you some clue as to what ItemView, CollectionView, and Layout are and how they can help you remove some of that boiler plate code
//one of these will be rendered out for each search result.
var SearchResult = Backbone.Marionette.ItemView.extend({
template: "#someTemplateRepresentingEachSearchResult"
)};
//This collectionview will render out a SearchResult for every model in it's collection
var SearchResultsView = Backbone.Marionette.CollectionView.extend{
itemView: SearchResult
});
//This layout will set everything up
var SearchWindow = Backbone.Marionette.Layout.extend({
template: "#someTemplateWithASearchBoxAndEmptyResultsRegionContainer",
regions:{
resultsRegion: "#resultsRegion"
},
initialize: function(){
this.foundUsers = new Users();
this.allUsers = new Users();
this.allUsers.fetch({
//snip...
});
events: {
'keyup #search-users-entry': 'onSearchUsers'
},
onSearchUsers: function(e){
var searchTerm = ($(e.currentTarget).val()).toLowerCase()
var results = this.allUsers.filter(function(user){
var firstName = user.attributes.firstname.toLowerCase();
return firstName.match(new RegExp(searchTerm))
});
this.foundUsers.set(results); //the collectionview will update with the collection
},
onRender: function(){
this.resultsRegion.show(new SearchResultsView({
collection: this.foundUsers
});
}
});
I think the most important thing for you to take note of is how CollectionView leverages the Backbone.Collection that you provide it. CollectionView will render out an itemView (of the class/type you give it) for each model that is in it's collection. If the Collection changes then the CollectionView will also change. You will notice that in the method onSearchUsers all you need to do is update that collection (using set). The CollectionView will be listening to that collection and update itself accordingly

can't get application to work after namespacing

I have a Backbone app that is working properly, however, when I tried to reorganize the code under a namespace I can't get it to do anything. I can't even trigger events (by clicking on ids) for views that I know are getting initialized (through console log messages), so I'm wondering if I've introduced some fundamental flaw somehow. I'm following a pattern set out by this blog (in french) http://www.atinux.fr/2011/12/10/organiser-son-code-backbone-js-en-modules/
In the main application.js (see below), I instantiate all of the views and models after initiating the app on document ready. One change introduced as a result of creating this namespace was setting the models for the views with this.models.game
this.views.clock_view = new this.Views.clockView({ model: this.models.game});
Inside the modules folder, I had a views.js and a models.js. I created each view and object like this, prefaced with app.Views or app.Models accordingly
app.Views.announceView = Backbone.View.extend({
....
app.Views.optionsView = Backbone.View.extend({
...
This app.Views.optionsView is getting initialized (according to a console.log statement in the initializer) but when I click on #new_game, the console.log in the startNewGame is not getting triggered
'click #new_game': 'startNewGame'
// 'click .action_button': 'startNewGame'
},
startNewGame: function() {
console.log("startNewGame");
this.model.new();
},
As a result of the namespacing, one other key change I made was when I created new views inside one of the other views. Under the previous (non-namespaced app), I created individual question items from a QuestionListView
var view = new QuestionListItemView({ model: game });
but now I'm doing
var view = new app.Views.questionListItemView({ model: app.models.game })
because the instance of the model was saved to this.models.game in application.js, however, I also tried using 'this.models.game'
var view = new app.Views.questionListItemView({ model: this.models.game })
Either way, before the games model gets involved, I can't trigger the startNewGame function outlined above, so it's not solely an issue of how to identify the model.
I also wondered whether i should be using this.Views or app.Views after the 'new' when creating new views from within
var view = new app.Views.questionListItemView({ model: this.models.game })
I'd be grateful if you could help me identify any flaws I've introduced.
application.js
var app = {
// Classes
Collections: {},
Models: {},
Views: {},
// Instances
collections: {},
models: {},
views: {},
init: function () {
this.models.game = new this.Models.game();
this.views.story_view = new this.Views.storyView(); #doesn't have a model
this.views.clock_view = new this.Views.clockView({ model: this.models.game});
this.views.field_view = new this.Views.fieldView({ model: this.models.game});
this.views.options_view = new this.Views.optionsView({ model : this.models.game});
this.views.announcement_view = new this.Views.announceView({ model: this.models.game});
this.views.question_list_view = new this.Views.questionListView({ model : this.models.game});
this.views.question_list_item_view = new this.Views.questionListItemView({ model : this.models.game});
}
};
$(document).ready(function () {
app.init();
}) ;
The options view is getting initialized but I can't trigger the startNewGame function when I click that #id
app.Views.optionsView = Backbone.View.extend({
// el: $("#options"),
el: $("#options"),
initialize: function() {
console.log("app views OptionsView initialized");
// this.model.bind("gameStartedEvent", this.removeGetAnswerButton, this);
this.model.bind("gameStartedEvent", this.disableNewGame, this);
},
events: {
'click #demo': 'startDemo',
'click #new_game': 'startNewGame'
// 'click .action_button': 'startNewGame'
},
startDemo: function(){
console.log("start demo");
this.model.demo();
},
startNewGame: function() {
console.log("startNewGame");
this.model.new();
},
disableNewGame: function(){
$('#new_game').attr("disabled", true);
}
});
Update
My file structure looks like this
<%= javascript_include_tag 'application.js'%>
<%= javascript_include_tag 'modules/models'%>
<%= javascript_include_tag 'modules/views'%>
At the top of the views and models file, I just do something like this
app.Views.optionsView = Backbone.View.extend({
ie.. there is no further document ready. In fact, including another document ready in these files breaks the application.js init
Prior to using the namespace, I defined the element this way in the view
el: $("#options")
which, as was pointed out in the comments to this question, is not the ideal way to do it(see #muistooshort comment below), (even though it worked).
Defining the el this way instead
el: '#options'
got it working, and let Backbone "convert it to a node object" on its own.

Multiple backbone views referencing one collection

I am trying to create my first backbone app and am having some difficulty getting my head around how I am meant to be using views.
What I am trying to do is have a search input that each time its submitted it fetches a collection from the server. I want to have one view control the search input area and listen to events that happen there (a button click in my example) and another view with sub views for displaying the search results. with each new search just prepending the results into the search area.
the individual results will have other methods on them (such as looking up date or time that they where entered etc).
I have a model and collection defined like this:
SearchResult = Backbone.model.extend({
defaults: {
title: null,
text: null
}
});
SearchResults = Backbone.Collection.extend({
model: SearchResult,
initialize: function(query){
this.query = query;
this.fetch();
},
url: function() {
return '/search/' + this.query()
}
});
In my views I have one view that represents the search input are:
var SearchView = Backbone.View.extend({
el: $('#search'),
events: {
'click button': 'doSearch'
},
doSearch: function() {
console.log('starting new search');
var resultSet = new SearchResults($('input[type=text]', this.el).val());
var resultSetView = new ResultView(resultSet);
}
});
var searchView = new SearchView();
var ResultSetView = Backbone.View.extend({
el: $('#search'),
initialize: function(resultSet) {
this.collection = resultSet;
this.render();
},
render: function() {
_(this.collection.models).each(function(result) {
var resultView = new ResultView({model:result});
}, this);
}
});
var ResultView = Backbone.view.extend({
tagName: 'div',
model: SearchResult,
initialize: function() {
this.render();
},
render: function(){
$(this.el).append(this.model.get(title) + '<br>' + this.model.get('text'));
}
});
and my html looks roughly like this:
<body>
<div id="search">
<input type="text">
<button>submit</button>
</div>
<div id="results">
</div>
</body>
In my code it gets as far as console.log('starting new search'); but no ajax calls are made to the server from the initialize method of the ResultSetView collection.
Am I designing this right or is there a better way to do this. I think because the two views bind to different dom elements I should not be instantiating one view from within another. Any advice is appreciated and if I need to state this clearer please let me know and I will do my best to rephrase the question.
Some problems (possibly not the only ones):
Your SearchView isn't bound to the collection reset event; as written it's going to attempt to render immediately, while the collection is still empty.
SearchView instantiates the single view ResultView when presumably it should instantiate the composite view ResultSetView.
You're passing a parameter to the SearchResults collection's constructor, but that's not the correct way to use it. See the documentation on this point.
You haven't told your ResultSetView to listen to any events on the collection. "fetch" is asynchronous. When completed successfully, it will send a "reset" event. Your view needs to listen for that event and then do whatever it needs to do (like render) on that event.
After fixing all the typos in your example code I have a working jsFiddle.
You see like after clicking in the button an AJAX call is done. Of course the response is an error but this is not the point.
So my conclusion is that your problem is in another part of your code.
Among some syntax issues, the most probable problem to me that I see in your code is a race condition. In your views, you're making an assumption that the fetch has already retrieved the data and you're executing your views render methods. For really fast operations, that might be valid, but it gives you no way of truly knowing that the data exists. The way to deal with this is as others have suggested: You need to listen for the collection's reset event; however, you also have to control "when" the fetch occurs, and so it's best to do the fetch only when you need it - calling fetch within the search view. I did a bit of restructuring of your collection and search view:
var SearchResults = Backbone.Collection.extend({
model: SearchResult,
execSearch : function(query) {
this.url = '/search/' + query;
this.fetch();
}
});
var SearchView = Backbone.View.extend({
el: $('#search'),
initialize : function() {
this.collection = new SearchResults();
//listen for the reset
this.collection.on('reset',this.displayResults,this);
},
events: {
'click button': 'doSearch'
},
/**
* Do search executes the search
*/
doSearch: function() {
console.log('starting new search');
//Set the search params and do the fetch.
//Since we're listening to the 'reset' event,
//displayResults will execute.
this.collection.execSearch($('input[type=text]', this.el).val());
},
/**
* displayResults sets up the views. Since we know that the
* data has been fetched, just pass the collection, and parse it
*/
displayResults : function() {
new ResultSetView({
collection : this.collection
});
}
});
Notice that I only created the collection once. That's all you need since you're using the same collection class to execute your searches. Subsequent searches only need to change the url. This is better memory management and a bit cleaner than instantiating a new collection for each search.
I didn't work further on your display views. However, you might consider sticking to the convention of passing hashes to Backbone objects. For instance, in your original code, you passed 'resultSet' as a formal parameter. However, the convention is to pass the collection to a view in the form: new View({collection: resultSet}); I realize that that's a bit nitpicky, but following the conventions improves the readability of your code. Also, you ensure that you're passing things in the way that the Backbone objects expect.

Can't get iscroll4 to play with backbone view

I am trying to use iScroll4 inside a backbone.js application. I have several dynamically loaded lists, and I want to initialize iScroll after the appropriate view has loaded.
I'm trying to call 'new iScroll' when the list view finishes loading, but cannot for the life of me figure out how to do this.
Has anyone gotten these two to work together? Is there an example out there of a backbone view initializing a scroller once its element has loaded?
you are correct, you have to load the view first,
or defenately refresh iscroll afterwards
in our applications, we usually use the render method to render the view
and have a postRender method that handles initialization of these extra plugins like iscroll
of course you need some manual work to get it done but this is the gist of it:
var myView = Backbone.View.extend({
// more functions go here, like initialize and stuff... but I left them out because only render & postRender are important for this topic
// lets say we have a render method like this:
render: function() {
var data = this.collection.toJSON();
this.$el.html(Handlebars.templates['spotlightCarousel.tmpl'](data));
return this;
},
// we added the postRender ourself:
postRender: function() {
var noOfSlides = this.collection.size();
$('#carouselscroller').width(noOfSlides * 320);
this.scroller = new IScroll('carouselwrapper', {
snap: true,
momentum: false,
hScrollbar: false
});
}
});
now the calling of these methods
we did this outside our view as we like some view manager to handle this
but it boils down to this
var col = new myCollection();
var view = new myView({ collection: col });
$('#wrapper').html(view.render().$el); // this chaining is only possible due to the render function returning the whole view again.
// here we always test if the view has a postRender function... if so, we call it
if (view.postRender) {
view.postRender();
}

Resources