Backbone, requirejs, multiple views, one collection - backbone.js

Below you can see four basic requireJS files. How can I have multiple Backbone.js views that will all share one collection which has been initiated and fetched elsewhere?
Please note
I am aware I can pass the collection in App.js however I would like to refrain from doing so since I will probably have many collections that will need to be used in many views, and I don't want to pass each of them in App.js.
Collection.js
return Backbone.Collection.extend({
url: '...'
});
App.js
var collection = new Collection();
$.when(collection.fetch()).done(function(){
new View1();
new View2();
});
View1.js
define(['Collection'], function(Collection){
return Backbone.View.extend({
initialize: function(){
console.log('Need the initiated, fetched collection here...');
});
});
});
View2.js
define(['Collection'], function(Collection){
return Backbone.View.extend({
initialize: function(){
console.log('Need the initiated, fetched collection here...');
});
});
});

Quick answer: RequireJS runs function body code once and the return statement alone multiple times. You can, hence, create a Collection Instance and return that to every person who requires it.
Collection.js
var CollectionConstructor = Backbone.Collection.extend({
url: '...'
});
var CollectionInstance = new CollectionConstructor();
return CollectionInstance;
Alternatively, if you want more than one instance of CollectionInstance running around, and don't want to pass it to the views, I don't believe anything short of global variables will help. That would look like:
Globals.js
var AppGlobals = AppGlobals || {};
AppGlobals.oneSet = new Collection ();
AppGlobals.anotherSet = new Collection ();
You can now have your views depend on Globals.js and access them from here. Depending on your use, either of these two should work. Keep in mind that in the second approach, your Collection.js is unmodified.

Related

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

backbone.js - how can i fetch local or server data

I am very new to backbone.js, to try it out I made a function to append an element using my array. But I don't know how to fetch the same from server or my local path.
I tried with some tutorial but still I couldn't get any good result. Can any one correct my function to fetch the data from local folder or server?
code :
this is my local path: '..data/data.json'
(function($){
var student = [
{name:'student1'},
{name:'student2'},
{name:'student3'}
]
var model = Backbone.Model.extend({
defaults:{
name:'default name'
}
});
var collection = Backbone.Collection.extend({
model:model
});
var itemViews = Backbone.View.extend({
tagname:'li',
render:function(){
this.$el.html(this.model.get('name'));
return this;
}
})
var view = Backbone.View.extend({
el: $("#contacts"),
initialize:function(){
this.collection = new collection(student);
this.render();
},
render:function(){
var that = this;
_.each(this.collection.models, function(item){
that.renderName(item);
})
},
renderName:function(item){
var itemView = new itemViews({model:item});
this.$el.append(itemView.render().el);
}
});
Collections have the url property which links to the url where the models can be fetched (in a proper JSON format of course).
If you have a RESTful API you can tie them directly to a collection on your backend.
In your case you can modify the collection definition as follows
var collection = Backbone.Collection.extend({
model:model,
url: '..data/data.json' //OR WHATEVER THE URL IS
});
Then when instantiating your collection you have to call the fetch() method to fill in the collection.
myCollection = new collection();
myCollection.fetch();
For more information see this post
Backbone will call GET, POST, DELETE, etc on that same url depending on what needs to be sent or received from the server. So if you are building the REST api, your controller or router should route the appropriate functions to those methods. See below for exact mapping:
create → POST /collection
read → GET /collection[/id]
update → PUT /collection/id
delete → DELETE /collection/id
As a side note, if you have no control over the JOSN that is returned from the server, you can format it in any format you like in the parse function.
parse: function(response) { return response.itemYouWantAdded; }
This would be the case if your server is returning an object within an object, such as twitter feeds.
Or you could simply populate the model from the response manually in the parse function
parse: function(response) { var tmpModel = {
item1:response.item1,
item2:response.item2
};
return tmpModel; }
Hope this helps.
You can set a URL for your model or for your collection. Then when you run fetch it will read your JSON straight into either a single model or a collection of models. So in your case, your collection might look like
var collection = Backbone.Collection.extend({
url: '..data/data.json',
model: model
});
collection.fetch(); // Could also use collection.reset()
You just need to make sure your JSON is formatted properly to match your model's attributes.

mass-write using a backbone collection

Using backbone.js, say I had a collection -- Albums
var albums_collection = new Backbone.Collection([
{artist:"dell the funky homosapien"},
{artist:"raffe"},
{artist:"wilson philips"},
{artist:"eddie murply"},
{artist:"jordotech"}
]);
And corresponding view
AlbumView = Backbone.View.extend({
render: function(){
$(this.el).html(this.model.get('artist') + "<br />");
return this;
}
});
Let's say, instead of just these 4 albums, I had 10k albums and I wanted to render them all. Most backbone tutorials would say you should loop through the collection and append to the dom one by one.
album_colleciton.each(addOne);
addOne:function(model){
var view = new AlbumView({model:model});
$("#albums_container").append(view.render().el):
}
However, it's come to my attention that writing to the DOM one by one like this might actually slow down performance... Is there some way to save each one of these to perhaps an array and mass update all at once? I've tried:
var arr = [];
_.each(this.collection.models, function(model){
var view = new AlbumView({model:model});
arr.push(view.render().el);
});
$("#albumbs_view").html(arr.join(''));
But the above results in a series of "HTMLdivElement"'s being rendered. Any idea how to do just one single instead of 1000 in this case?
make sure your view does NOT specify an "el". allow that to be create by backbone. then your render function can populate the view's el all day long without it manipulating the DOM. finally, when you're done, attach it to the DOM.
CollectionView = Backbone.View.extend({
render: function(){
var that = this;
this.collection.each(function(m){
var v = new SomeView({model: m});
v.render();
that.$el.append(v.$el);
});
}
});
cv = new CollectionView({collection: someCollection});
cv.render();
$("#whatever").html(cv.$el);
By the way, my Backbone.Marionette framework will make this even easier for you:
SomeView = Marionette.ItemView.extend({
// ...
});
AllTheViews = Marionette.CollectionView.extend({
itemView: SomeView
});
atv = new AllTheViews({collection: someCollection});
atv.render();
$("#whatever").html(atv.$el);
And then there's Regions which make stuffing the view in to the DOM even easier... A good place to start if you're interested in Marionette is Addy Osmani's Backbone Fundamentals book, and the chapter on Marionette.

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.

Backbone.js: retrieving a collection from the server in PHP

I'm having a look at Backbone.js, but I'm stuck. The code until now is as simple as is possible, but I seem not to get it. I use Firebug and this.moments in the render of MomentsView is an object, but all the methods from a collection don't work (ie this.moments.get(1) doesn't work).
The code:
var Moment = Backbone.Model.extend({
});
var Moments = Backbone.Collection.extend({
model: Moment,
url: 'moments',
initialize: function() {
this.fetch();
}
});
var MomentsView = Backbone.View.extend({
el: $('body'),
initialize: function() {
_.bindAll(this, 'render');
this.moments = new Moments();
},
render: function() {
_.each(this.moments, function(moment) {
console.log(moment.get('id'));
});
return this;
}
})
var momentsview = new MomentsView();
momentsview.render();
The (dummy) response from te server:
[{"id":"1","title":"this is the moment","description":"another descr","day":"12"},{"id":"2","title":"this is the mament","description":"onother dascr","day":"14"}]
The object has two models according to the DOM in Firebug, but the methods do not work. Does anybode have an idea how to get the collection to work in the view?
The problem here is that you're fetching the data asynchronously when you initialize the MomentsView view, but you're calling momentsview.render() synchronously, right away. The data you're expecting hasn't come back from the server yet, so you'll run into problems. I believe this will work if you call render in a callback to be executed once fetch() is complete.
Also, I don't think you can call _.each(this.moments) - to iterate over a collection, use this.moments.each().
Try removing the '()' when instantiate the collection.
this.moments = new Moments;
Also, as it's an asynchronous call, bind the collection's 'change' event with the rendering.
I hope it helps you.

Resources