BackboneJS nested view not rendering - backbone.js

I have backbonejs form inside the lightbox with html <select> as child view.
For the select <option> data I am loading from server and I have separate model and collection for this select
<select name="organization" id="organization" class="main__form--select main__form--select--js">
<option value="no">Organizations not found, Please add one</option>
</select>
Model for option (optionModel)
return Backbone.Model.extend({
defaults : {
"name" : 'KFC',
"email" : 'info#kfc.com',
"image" : '/kfc.jpg',
"descrption" : 'Lorem Ipsum'
}
});
This is view for the model
return Backbone.View.extend({
model : optionModel,
template : _.template(template),
render : function () {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
This is options collection
return Backbone.Collection.extend({
model : optionModel,
getQuery : function(){
//all my query codes
}
});
Options collections view render() code
this.collection.each(function (optionModel) {
// inserting each options view to an array
_this._optionsViewArray.push(
new OptionView({
model: optionModel
}).render().el
);
});
//inserting array to collection view container
_this.$el.html(_this._optionsViewArray);
return this;
My Parent view (form view) i create after render function with underscore _.wrap and inside that function
//<select>
var _selector = this.$el.find('#organization');
optionsView = new OptionsCollectionView({
collection : optionsCollection,
$el: _selector
});
optionsCollection.getQuery();
optionsView.render();
But Form is loading perfectly and Options collection querying successfully but nothing changes on <select> html, It's not updating.

Assuming that getQuery() does an asynchronous query (either using jQuery or Collection.fetch()), the problem (or at least one problem) is that you're calling optionsView.render() to soon, before the query results have returned.
You could throw in an arbitrary 5 second timeout to verify that this is the problem like this: setTimeout(function() { optionsView.render(); }, 5 * 1000);, but the correct way to do it is to call render from a jQuery done handler function ($.ajax({done: function() { ... }})) or (better) a Backbone sync event handler (this will only get called if you did the query via a Backbone collection fetch()): optionsCollection.on('sync', function() { ... });

You example is incomplete. However, given that fetches are generally asynchronous in Backbone and jQuery, this is the most obvious issue is that optionsView.render will run before optionsCollection has had a chance to fully load the collection. You can remedy this by listening to the collection update event:
optionsView = new OptionsCollectionView({
collection : optionsCollection,
$el: _selector
});
optionsView.listenTo(optionsCollection, 'update', optionsView.render);
optionsCollection.getQuery();
You could listen to the sync event instead, but listening to update means that local changes to the collection will also trigger updating your view.
There is also an issue with your render function, as you are passing an array of html elements into the jQuery html function.
render: function() {
var $el = this.$el;
$el.empty();
this.collection.each(function (optionModel) {
var view = new OptionView({ model: optionModel });
$el.append(view.render().$el);
});
return this;
}
However you may be better off using a standard collection view pattern, or something like Backbone.CollectionView - http://rotundasoftware.github.io/backbone.collectionView/

Related

Marionette ItemView not re-rendering on model change from collection view

I have created a collection passing a collection view and a collection. The collection references a model I have created. when fetching the collection the items get rendered succesfully, but when the models change, the itemViews are not being re-rendered as expected.
for example, the itemAdded function in the tweetCollectionView is called twice ( two models are added on fetch ) but even though the parse function returns different properties over time for those models ( I assume this would call either a change event on the collection, or especially a change event on the model, which I have tried to catch in the ItemView ) the itemChanged is never called, and the itemViews are never re-rendered, which i would expect to be done on catching the itemViews model change events.
The code is as follows below:
function TweetModule(){
String.prototype.parseHashtag = function() {
return this.replace(/[#]+[A-Za-z0-9-_]+/g, function(t) {
var tag = t;
return "<span class='hashtag-highlight'>"+tag+"</span>";
});
};
String.prototype.removeLinks = function() {
var urlexp = new RegExp( '(http|ftp|https)://[\w-]+(\.[\w-]+)+([\w.,#?^=%&:/~+#-]*[\w#?^=%&/~+#-])?' );
return this.replace( urlexp, function(u) {
var url = u;
return "";
});
};
var TweetModel = Backbone.Model.extend({
idAttribute: 'id',
parse: function( model ){
var tweet = {},
info = model.data;
tweet.id = info.status.id;
tweet.text = info.status.text.parseHashtag().removeLinks();
tweet.name = info.name;
tweet.image = info.image_url;
tweet.update_time_full = info.status.created_at;
tweet.update_time = moment( tweet.update_time_full ).fromNow();
return tweet;
}
});
var TweetCollection = Backbone.Collection.extend({
model: TweetModel,
url: function () {
return '/tweets/game/1'
}
});
var TweetView = Backbone.Marionette.ItemView.extend({
template: _.template( require('./templates/tweet-view.html') ),
modelEvents:{
"change":"tweetChanged"
},
tweetChanged: function(){
this.render();
}
})
var TweetCollectionView = Marionette.CompositeView.extend({
template: _.template(require('./templates/module-twitter-feed-view.html')),
itemView: TweetView,
itemViewContainer: '#tweet-feed',
collection: new TweetCollection([], {}),
collectionEvents: {
"add": "itemAdded",
"change": "itemChanged"
},
itemAdded: function(){
console.log('Item Added');
},
itemChanged: function(){
console.log("Changed Item!");
}
});
this.startInterval = function(){
this.fetchCollection();
this.interval = setInterval( this.fetchCollection, 5000 );
}.bind(this);
this.fetchCollection = function(){
this.view.collection.fetch();
this.view.render();
}.bind(this);
//build module here
this.view = new TweetCollectionView();
this.startInterval();
};
I may be making assumptions as to Marionette handles event bubbling, but according to the docs, I have not seen anything that would point to this.
Inside your CollectionView, do
this.collection.trigger ('reset')
after model have been added.
This will trigger onRender () method in ItemView to re-render.
I know I'm answering an old question but since it has a decent number of views I thought I'd answer it correctly. The other answer doesn't address the problem with the code and its solution (triggering a reset) will force all the children to re-render which is neither required nor desired.
The problem with OP's code is that change is not a collection event which is why the itemChanged method is never called. The correct event to listen for is update, which according to the Backbone.js catalog of events is a
...single event triggered after any number of models have been added
or removed from a collection.
The question doesn't state the version of Marionette being used but going back to at least version 2.0.0 CollectionView will intelligently re-render on collection add, remove, and reset events. From CollectionView: Automatic Rendering
When the collection for the view is "reset", the view will call render
on itself and re-render the entire collection.
When a model is added to the collection, the collection view will
render that one model in to the collection of child views.
When a model is removed from a collection (or destroyed / deleted),
the collection view will destroy and remove that model's child view
The behavior is the same in v3.

How to properly update subviews on Marionette when using constantly updating JSON

I have a single page app built with Marionette that has a main view with a list of subviews.
The JSON which holds all application data is updated constantly. I've tried to separate region show code so that it will be run just once and not on every render.
Now the render event is fired on every timeout loop even though the JSON is static data and therefore change event should not call render. What is wrong? I assume it has something to do with .set but is there any other way to load the response from an array variable to the subview collection, since fetch will allow only url attribute and will not accept array variable?
This example is an extremely simplified version of the application code to concentrate on this specific problem.
Controller:
var Controller = {
showMainView: function(id){
// create model and fetch data on startup
var mainElement = new mainElement();
var mainElementFetched = mainElement.fetch({url: 'http://json.json'});
// fetch done, create view, show view in region, setTimeout
mainElementFetched.done(function(data){
var mainElementView = mainElementView({model:mainElement});
App.mainRegion.show(mainElementView);
setTimeout(updateJSON, 5000);
}
// timeOut loop to check for JSON changes
var updateJSON = function(){
mainElement.fetch({url: 'http://json.json'});
App.timeOut = setTimeout(updateJSON, 5000);
}
}
}
MainElement Model:
MainElement = Backbone.Model.extend({
parse : function(response){
// parsing code
return response;
}
});
MainElementView (Layout):
MainElementView = Backbone.Marionette.Layout.extend({
template: "#main-template",
initialize:function(){
//create collection for subelements
this.subElementCollection = new SubElementCollection();
//listen to change event, and fire callback only when change in model is detected
this.model.on('change', this.render, this);
},
regions:{
subsection : ".subsection"
},
onShow: function(){
// show subelements in subsection region when mainelementview is shown on screen, but not show on every render
this.subsection.show(new SubElementCompositeView({collection:this.subElementCollection}))
},
onRender : function(){
var response = this.model.response;
// get subelements when change event fires and parse the response
this.subElementCollection.set(response,{parse:true});
}
});
SubElement Model, Collection, ItemView, CompositeView:
SubElement = Backbone.Model.extend({});
SubElementCollection = Backbone.Collection.extend({
model:SubElement,
comparator : function(model){
var price = model.get('price');
return -(price);
},
parse:function(response){
// parsing code to get data to models from differents parts of JSON
return response;
}
});
SubElementItemView = Backbone.Marionette.ItemView.extend({
template: "#subelement-template",
tagName: "tr"
});
SubElementCompositeView = Backbone.Marionette.CompositeView.extend({
template: "#subelements-template",
tagName : "table",
itemView:SubElementItemView,
itemViewContainer : "tbody",
initialize: function(){
this.collection.on('change', this.render, this);
},
appendHtml : function(collectionView,itemView,index){
// SORTING CODE
},
onRender:function(collectionView,itemView,index){
// ADD IRRELEVANT EXTERNAL STUFF TO TEMPLATE AFTER RENDER
}
});
Check out the documentation. It says:
A "change" event will be triggered if the server's state differs from the current attributes.
In your MainElementView where you bind to your model's change event: this.model.on('change', this.render, this);, you are actually saying every time your model changes, call render.
If re-drawing the whole view is too slow, due to the amount of changes. Why don't you make your rendering more fine-grained? For example you could listen for specific change events and just change the DOM elements which need changing:
this.model.on('change:Name', function () {
this.$('[name=Name]').html(this.model.get('Name'));
}, this);
It is more work to set this up, but you could make it a bit cleverer by matching the model property names to your DOM element name or something.

How does Chaplin.js handle passing a collection to a view?

I can create a simple model like so:
define(["models/base/model"], function(Model) {
"use strict";
var IssueModel = Model.extend({
defaults:{
lastName: "Bob",
firstName: "Doe"
}
});
return IssueModel;
});
And then from my controller I can do this:
this.model = new IssueModel();
And then when I create my view I can pass it my model like so:
this.view = new IssueView({model: this.model});
Finally, in my template I can successfully get properties on the model by doing this:
Hi {{firstName}} {{lastName}}
But when I define a collection using IssueModel and I try to pass the collection to my view (and not the model like I showed previously) I can't figure out how to reference the models in my Handlebars template:
var self = this;
this.collection = new IssueCollection();
this.collection.fetch({
success: function(collection) {
self.view = new IssueView({collection: collection});
console.log(self.collection);
},
error: function(collection, error) {
// The collection could not be retrieved.
}
});
I know fetch properly retrieves 5 models from my Parse.com backend because this is what I get on the console:
My question is this. I know Chaplin.js uses getTemplateData, but when I pass a model I don't have to do anything special in order to reference the properties in my view. How would I reference, specifically iterate, over the collection I passed to my view in my Handlebars template?
{{#each [Model in the collection I passed to the view]}}
{{title}}
{{/each}}
Chaplin will render a collection using a CollectionView, it's basicly an extention of a normal view that listens for changes in your collection and adds/removes subviews accordingly.
this.view = new IssueCollectionView({collection: this.collection});
Also there is no need to wait for success call when using a collection view since it will automaticly render every child item when data is added.

Backbone.js - fetch method does not fire reset event

I'm beginning with Backbone.js and trying to build my first sample app - shopping list.
My problem is when I fetch collection of items, reset event isn't probably fired, so my render method isn't called.
Model:
Item = Backbone.Model.extend({
urlRoot : '/api/items',
defaults : {
id : null,
title : null,
quantity : 0,
quantityType : null,
enabled : true
}
});
Collection:
ShoppingList = Backbone.Collection.extend({
model : Item,
url : '/api/items'
});
List view:
ShoppingListView = Backbone.View.extend({
el : jQuery('#shopping-list'),
initialize : function () {
this.listenTo(this.model, 'reset', this.render);
},
render : function (event) {
// console.log('THIS IS NEVER EXECUTED');
var self = this;
_.each(this.model.models, function (item) {
var itemView = new ShoppingListItemView({
model : item
});
jQuery(self.el).append(itemView.render().el);
});
return this;
}
});
List item view:
ShoppingListItemView = Backbone.View.extend({
tagName : 'li',
template : _.template(jQuery('#shopping-list-item').html()), // set template for item
render : function (event) {
jQuery(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
Router:
var AppRouter = Backbone.Router.extend({
routes : {
'' : 'show'
},
show : function () {
this.shoppingList = new ShoppingList();
this.shoppingListView = new ShoppingListView({
model : this.shoppingList
});
this.shoppingList.fetch(); // fetch collection from server
}
});
Application start:
var app = new AppRouter();
Backbone.history.start();
After page load, collection of items is correctly fetched from server but render method of ShoppingListView is never called. What I am doing wrong?
Here's your problem:
" When the model data returns from the server, it uses set to (intelligently) merge the fetched models, unless you pass {reset: true}" Backbone Docs
So, you want to fire the fetch with the reset option:
this.shoppingList.fetch({reset:true}); // fetch collection from server
As an aside, you can define a collection attribute on a view:
this.shoppingList = new ShoppingList();
this.shoppingListView = new ShoppingListView({
collection : this.shoppingList // instead of model: this.shoppingList
});
Are you using Backbone 1.0? If not, ignore this, otherwise, you may find what the doc says about the Collection#fetch method interesting.
To quote the changelog:
"Renamed Collection's "update" to set, for parallelism with the similar model.set(), and contrast with reset. It's now the default updating mechanism after a fetch. If you'd like to continue using "reset", pass {reset: true}"
So basically, you're not making a reset here but an update, therefore no reset event is fired.

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.

Resources