So here is my scenario:
I have a Backbone Collection full of Models. For performance reasons, however, these are not "full" Models. My "full" Models are quite large (imagine each "full" Model has a sub-collection of equally large objects), so when I fetch the Collection from the server I return an array of "partial" Models whose properties are a subset of the "full" model (for example I return only the length of the sub-collection instead of the full sub-collection), just enough to display the Models in a list view to the user.
Now when the user selects an item from the list, I fetch the "full" Model from the server and show a details view of that Model. The problem I have is that now I have two versions of the same Model, one "partial" in the Collection and one "full", and manually having to keep them in sync isn't the right way to do things.
What I'm wondering is if there is an existing pattern (in Backbone or Marionette) for "populating" a "partial" Model into a "full" Model while keeping all of the same references, and for "depopulating" the same Model from a "full" Model into a "partial" Model when we no longer need all of the extra data (i.e. the user navigates to another item in the list).
I have full control over both the front-end and the back-end of my application, and can make changes accordingly if a pattern requires I change what the server returns.
You are representing a single domain object (albeit in two different forms), so you should use a single Model instance to cover both cases.
One fairly clean pattern:
var MyModel = Backbone.Model.extend({
// ... existing code...
inflate: function() {
return $.ajax({
// parameters to fetch the full version
}).then(function(data) {
// process the response - something like this:
this.set({
a: data.a,
b: data.b
}, { silent: true })
this.trigger('inflate')
})
},
deflate: function() {
this.unset('a', { silent: true });
this.unset('b', { silent: true });
// any other cleanup to prevent leaking the large attributes
this.trigger('deflate')
}
})
This pattern uses the custom inflate and deflate events in preference to firing change, because it's semantically more accurate.
You could, of course, DRY up the code by maintaining an array of attribute names that should be in/deflated.
Just like your collection has a URL to the "partial" models, your models should have a URL to the full versions:
var Library = Backbone.Collection.extend({
model: Book,
url: "/books"
});
var Book = Backbone.Model.extend({
url: function () {
return "/books/" + this.get("id");
}
});
When you click your item view use that same model, call a fetch(), and pass it into the detail view.
var BookView = Backbone.View.extend({
tagName: "li",
events: {
"click .details": "openBook"
},
initialize: function() {
// ...
},
openBook: function () {
this.model.fetch();
var bookDetailView = new BookDetailView({ model: this.model });
// Or create the view after successful fetch...
}
// ...
});
var BookDetailView = Backbone.View.extend({});
You won't have two versions of the same model. The model in the collection view will now have all the attributes, but it will only display what is in the template.
As far as "depopulating" it doesn't seem necessary. If the item is clicked again you could even check if the "full" model data is available and lose the extra fetch. If you really want to drop the data, then go ahead and create a method on the model to unset the attributes.
Related
Say a user is going down a page and checking off and selecting items.
I have a Backbone model object, and each time the user selects something I want to update the object.
I have this in a separate JavaScript file that I source in my HTML:
var app = {};
var newLineup = null;
var team = document.getElementsByName('team');
app.Lineup = Backbone.Model.extend({
defaults: {
team: team,
completed: false
},
idAttribute: "ID",
initialize: function () {
console.log('Book has been intialized');
this.on("invalid", function (model, error) {
console.log("Houston, we have a problem: " + error)
});
},
constructor: function (attributes, options) {
console.log('document',document);
console.log('Book\'s constructor had been called');
Backbone.Model.apply(this, arguments);
},
validate: function (attr) {
if (attr.ID <= 0) {
return "Invalid value for ID supplied."
}
},
urlRoot: 'http://localhost:3000/api/lineups'
});
function createNewLineupInDatabase(){
newLineup = new app.Lineup({team: team, completed: false});
newLineup.save({}, {
success: function (model, respose, options) {
},
error: function (model, xhr, options) {
}
});
}
When the user first accesses the page, I will create a new lineup object by calling the above function. But how do I update that object as the user interacts with the page? Is there a better way to do this other than putting the Backbone model object at the top of my JavaScript file?
The Backbone pattern was designed to answer your question. As other respondents said, wire up a View, which takes your model as a parameter and lets you bind DOM events to the model.
That said, you don't have to use the rest of the framework. I guess you can use all the functionality Backbone provides models by handling the model yourself.
You need to worry about a couple of things.
Give you model a little encapsulation.
Set up a listener (or listeners) for your checkbox items.
Scope the model to your app
Backbone provides neat encapsulation for your model inside a View, but if you can live with it, just use your app variable which is within scope of the JavaScript file you posted.
When you're ready to instantiate your model, make it a property of app:
app.newLineup = new app.Lineup({team: team, completed: false});
It may look weird to have the instance and the constructor in the same object, but there aren't other options until you pull out the rest of Backbone.
The listener
So you have N number of checkboxes you care about. Say you give them a class, say, .options. Your listener will look like
$( ".options" ).change(function() {
if(this.checked) {
//Do stuff with your model
//You can access it from app.newLineup
} else {
}
});
Voila! Now your page is ready to talk to your model.
If there is frontend ui / any user interaction within your code it is extremely useful to create a backbone view which makes use of an events object where you can set up your event handler.
You can also link a view to a model to allow your model / your object to be updated without scope issues.
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
In my backbone model, I parse the response from the server:
var MyModel = Backbone.Model.extend({
urlRoot: "/users",
parse: function(response){
var data = {};
data.id = reponse.userDetails.id;
data.name = response.userDetails.firstname + " " + response.userDetails.lastname;
data.description = response.userDetails.description;
return data;
}
});
var myModel = new MyModel({id: 1});
myModel.fetch();
The views that use this model can manipulate it, for example, if the user were to click on the view to "select" it, it would update the model...
myModel.set({selected: true});
...and the view would re-render based on the model's change event and highlight the "selected" user.
When it comes time to save the model to the server, how do I only send the attributes the server wants? and ignore the attributes which were added through user interaction.
OR
Should the data model always reflect what the server returns? If so, is there a better way to store the user interactions (whether the view is "selected")? Should it be a separate model than the actual data model?
Thanks
The model doesn't need to mirror the data on the server if that doesn't make sense for your application.
For the model's attributes, if you don't need to render those attributes in a template, then you can just override model.toJSON() to only serialize the attributes you want sent to the server. Be careful though, in this case if you are rendering your template (or anything else) using this.model.toJSON() then it will also be affected. If that's a problem then you can override model.sync() instead and manipulate the data passed in before sending it to Backbone.sync. For example:
var myModel = Backbone.Model.extend({
sync: function (method, model, options) {
// remove the unwanted attributes. Something like...
options.attrs = _.pick(model.attributes, 'attribute1', 'attribute2', 'attribute3');
return Backbone.sync.call(this, method, model, options);
}
});
Overriding model.toJSON as suggested by mu_is_too_short worked nicely for me.
In the model
function() {
var json = Backbone.Model.prototype.toJSON.call(this);
json.ExtendedFieldData = JSON.stringify(json.ExtendedFieldData);
return json;
},
We use model.attributes for templates.
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.
I have a JSON file that will create my ParentModel as well as populate the child Records collection.
ParentModel : Backbone.Model.extend({
initialize: function() {
this.set({ records: new Records(this.get("records")) });
}
});
And the Records collection is just a basic Backbone collection that maps to a Record model.
The problem is that I need the child to know about the parent, so each Record model has to have a parent property on it. So for now I've just been adding this to the bottom of the initialize method:
var self = this;
this.get("records").each(function(record) {
record.set("parent", self);
});
This works fine, but when I'm creating a new record I'd rather not have to remember to include those 4 lines.
This answer says I can override the initialize method to take in additional parameters, but I'm not quite sure how I would get Backbone to automatically pass in the ParentModel to the overridden initialize method. Can anyone provide an example on how to do that?
I've heard of Backbone-relational which might help do what I want, but that's another 23kb to include. If that's the better way to go I'll look at implementing it, but otherwise I'd prefer a simpler solution if one is available.
This needs to work whether I create a new ParentModel record through code, or if it's being automatically created by a JSON feed.
I usually find that moving the structural elements out of the attributes is cleaner so my records and parent properties are on the object, not the attributes. That said, you could take advantage of the different events on the collection and the parent object:
var ParentModel = Backbone.Model.extend({
initialize: function () {
_.bindAll(this, 'adoptOne', 'adoptAll');
this.records = new Records();
this.on('change:records', function () {
this.records.reset(this.get('records'));
});
this.records.on('reset', this.adoptAll);
this.records.on('add', this.adoptOne);
this.records.reset(this.get('records'));
},
adoptAll: function () {
this.records.each(this.adoptOne);
},
adoptOne: function (model) {
model.parent = this;
}
});
A few tests :
var p = new ParentModel({
name: "I am the parent",
records: [{id: 1}, {id: 2}]
});
p.records.add({id: 3});
p.records.each(function (child) {
console.log(child.get('id')+' '+child.parent.get('name'));
});
p.set({records: [{id: 4}]});
p.records.each(function (child) {
console.log(child.get('id')+' '+child.parent.get('name'));
});
And a Fiddle http://jsfiddle.net/sPXaZ/
Just to be clear, here is a summary of one of your needs (which can be find in the question comments):
For example, if I wanted to center position a Record element, I'd need
to know how wide the viewbox is. The only way to know that is to know
what the widest record element is. The parent object can tell me that
by sorting through its child elements.
It seems to me that your Model have to handle display problematic; display in Backbone is handled by Views. So, I guess you can create a View which listen to both ParentModel and Records.
var RecordView = Backbone.View.extend({
initialize: function() {
this.collection.on('sync', this.render, this);
}
render: function() {
var widest = this.model.get('width');
}
});
var view = new RecordView({model: ParentModel, collection: Records});
And, in my opinion, this is not to the ParentModel to handle the width it take on screen but to its own View. Introducing two Views here seems to be the point.
But I've not the full picture, so, please, if I'm wrong, give me more samples of what you're trying to do.