Suppose I have models:
var Book = Backbone.Model.extend({
// ...
});
var Store = Backbone.Model.extend({
getBooks: function(){
this.books = new Books(App.Singletons.AllBooks.where({storeId: this.id}));
return this.books;
}
});
Store has many books.
Also suppose I have a collection:
var Books = Backbone.Collection.extend({
model: Book
});
In a complex business logic I need a base store object for the Books:
var App = App || {};
App.Singletons = App.Singletons || {};
var books = new Books();
books.fetch();
App.Singletons.AllBooks = books;
// ...
What the best way for syncing a singleton model for operations like this:
var store = new Store({id: 1});
store.fetch();
store.getBooks();
store.books.add(...);
store.books.remove(...);
// etc
// There I need to sync with App.Singletons.AllBooks
Now I'm overriding these methods in the Books collections. And there the App.Singletons.AllBooks is being synced.
I think there should be another better solution for this task.
Thanks for helping.
If you want to keep something in "sync", override the sync method.
In the case of store:
var Store = Backbone.Model.extend({
getBooks: function() {
this.books = new Books(App.Singletons.AllBooks.where({
storeId: this.id
}));
return this.books;
},
sync: function(method, model, options) {
switch (method) {
case 'create':
//your logic to sync with App.Singletons.AllBooks
break;
case 'read':
//your logic to sync with App.Singletons.AllBooks
break;
case 'update':
//your logic to sync with App.Singletons.AllBooks
break;
case 'delete':
//your logic to sync with App.Singletons.AllBooks
break;
}
}
Backbone.prototype.sync.call(this, method, method, options);
});
Related
I have a view with multiple collections, implemented like this:
collection: {
folders: new FolderCollection(),
images: new ImageCollection(),
files: new FileCollection()
},
And example collection is like this:
var FolderCollection = Backbone.Collection.extend({
model: folderObj,
initialize:function (){
// this.bindAll(this);
// this.setElement(this.at(0));
},
comparator: function(model) {
return model.get("id");
},
getElement: function() {
return this.currentElement;
},
setElement: function(model) {
this.currentElement = model;
},
next: function (){
this.setElement(this.at(this.indexOf(this.getElement()) + 1));
return this;
},
prev: function() {
this.setElement(this.at(this.indexOf(this.getElement()) - 1));
return this;
}
});
As you can imagine, this View is a display for files, images, and folders. I then populate the view by calling three different functions; one to populate the view with folders, another for files, and another for images. Each of these functions is a separate ajax request. So, because these calls are asynchronous, there's no way to first load folders, then images, then files and there is no consistency when the page loads.
So, my problem is, I need to be able to order these three collections in multiple ways. The first problem is, since the calls are async, sometimes the folders load first, or maybe the files, etc. I can think of two ways to fix this:
Only call the next function after the previous is completed. Is this the best way? If so, how do I do that
After all the collections are loaded, sort them. If so, how is the best way to sort and order multiple collections?
If more code is needed (ie: model or view) please let me know and I can provide what ever is needed.
thanks
jason
EDIT - SHOWING VIEW
var FileManagementView = TemplateView.extend({
viewName: 'fileManagement',
className: 'fileManagement',
events: {
//my events
},
collection: {
folders: new FolderCollection(),
images: new ImageCollection(),
files: new FileCollection()
},
//life cycle
initialize: function (options) {
TemplateView.prototype.initialize.apply(this, [options]);
},
templateContext: function (renderOptions) {
},
postRender: function () {
//more functions to set up the view
this.repopulateViewWithFoldersAndFiles(currentFolderId);
},
template: function (renderOptions) {
return 'MyMainTemplate';
},
repopulateViewWithFoldersAndFiles: function(currentFolderId){
//code to do stuff to create view
//these functions are all async, so theres no way to know what will finish first
this.getFolders(currentFolderId);
this.getImages();
this.getFiles();
},
getFiles: function(){
try{
var that = this;
var url = '?q=url to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getFiles'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var file = new fileObj;
file.set('id', value.id);
file.set('fileName', value.fileName);
//...set more attributes
that.collection.files.add(file);
that.renderFile(file);
});
}
});
} catch(e){
throw e;
}
},
renderFile: function(file){
try{
if(file) {
var template = window.app.getTemplate('AnotherTemplate');
var html = $(template({
id: file.get('id'),
fileName: file.get('fileName'),
fileIconPath: file.get('fileIconPath')
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
},
getImages: function(){
try{
var url = '?q=url to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getImages'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var image = new imageObj;
image.set('id', value.id);
image.set('imgTitle', value.image_name);
//....set more attributes
that.collection.images.add(image);
that.renderImage(image);
});
}
});
} catch(e){
throw e;
}
},
renderImage: function(image){
try{
if(image) {
var template = window.app.getTemplate('myViewTemplate');
var html = $(template({
imgId: image.get('id'),
imgTitle: image.get('imgTitle'),
//..more attributes
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
},
getFolders:function(parentId){
var that = this;
var url = '?q=...path to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getFolders'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var folder = new folderObj();
folder.set('folderName', value.folder_name);
folder.set('id', value.folder_id);
//more attributes
that.collection.folders.add(folder);
that.renderFolders(folder);
});
}else{
this.renderFolders(null);
}
});
},
//renders the folders to the view
renderFolders: function(folder){
try{
if(folder) {
var template = window.app.getTemplate('myFolderTemplate');
var html = $(template({
folderId: folder.get('id'),
folderName: folder.get('folderName'),
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
}
});
What I ended up doing was rewriting my models and creating one model that the others inherit from. Example:
var DataModel =MyBaseModel.extend({
defaults: {
id: null,
//other shared fields
}
});
All my other models inherited, like this:
var folderObj = DataModel.extend({
// Whatever you want in here
urlRoot: '?q=myApp/api/myClassName/',
defaults: {
//other fields here
},
validate: function(attributes){
//validation here
}
});
I then used deferred, which I answered here: Jquery Promise and Defered with returned results
I am creating a single page app that lets users filter for data upon two criteria (Skills and Location). This data is to be populated from two separate web services.
There is a model for each service to consume the data using REST style requests.
I want to use both bits of data in this one view. From my understanding a collection can hold multiple instances of one type of Model e.g. "Movie"
var Movies = Backbone.Collection.extend({
model: Movie,
initialize: function() {
console.log("");
console.log("Movie Collection initialize");
console.log(this);
console.log(this.length);
console.log(this.models);
}
});
var movie1 = new Movie({
"title": "Bag It",
"averageUserRating": 4.6,
"yearReleased": 2010,
"mpaaRating": "R"
});
var movie2 = new Movie({
"title": "Lost Boy: The Next Chapter",
"averageUserRating": 4.6,
"yearReleased": 2009,
"mpaaRating": "PG-13"
});
However I am trying to implement the pattern below, where the collection has two Models. Is this an anti pattern for Backbone. How should this be tackled?
define([
'underscore',
'backbone',
'models/locationsModel',
'models/skillsModel'
], function (_, Backbone, Location, Skills)
{
'use strict';
var FiltersCollection = Backbone.Collection.extend({
// The filters collection requires these two models that will provide data to the filters view
location: new Location(),
skills: new Skills(),
initialize: function() {
//Do stuff
}
});
return new FiltersCollection();
});
I can't advise on what is best for you because I can't visualise your data properly based on the info provided. But if you observe the collection constructor in the Backbone source:
if (options.model) this.model = options.model;
Then in _prepareModel:
var model = new this.model(attrs, options);
And we knew that "model" is a function anyway, and a function can return what you want. So providing your two different data sources have some attribute that can identify them you can do something like this:
var SkillModel = Backbone.Model.extend({
sayMyName: function() {
return 'I am a skill model and I am skilled at ' + this.get('name');
}
});
var LocationModel = Backbone.Model.extend({
sayMyName: function() {
return 'I am a location model and I am relaxing in ' + this.get('name');
}
});
function FilterModel(attrs, options) {
if (attrs.type === 'skill') {
return new SkillModel(attrs, options);
} else if (attrs.type === 'location') {
return new LocationModel(attrs, options);
}
}
var FilterCollection = Backbone.Collection.extend({
model: FilterModel
});
var filteredCollection = new FilterCollection([{
type: 'skill',
name: 'carpentry'
}, {
type: 'location',
name: 'India'
}, {
type: 'skill',
name: 'plumbing'
}]);
var outputEl = document.querySelector('#output');
filteredCollection.each(function(model) {
outputEl.innerHTML += '<p>' + model.sayMyName() + '<p>';
});
<script src="http://underscorejs.org/underscore.js"></script>
<script src="http://backbonejs.org/backbone.js"></script>
<div id="output"></div>
I have a list models like Product, Book, Phone some of them may inherit from another.
And I try to build a from to add different products, the form view should be changed according to the type chosen by user.
This is what have tried now:
var Product = Backbone.Model.extend({
defaults: {
name: null,
price: 0,
type: null, /** phone or book or something else*/
phoneColor: null,
phoneWeight: 0,
bookCategory: null,
bookAuthor: null
}
});
var ProductFormView = Backbone.View.extend({
..........
render: function () {
var type = this.model.get('type');
switch (type) {
case 'phone':............
break;
case 'book':.........
break;
}
return this;
}
});
Put all the attributes to the Product, and render the form according to different model type, live demo: http://jsfiddle.net/57ra37vg/
Even it works now, but I think this is a bad idea, in my opinion, the base class Product should not contain too many attributes. I'd like something like this:
var Product = Backbone.Model.extend({
defaults: {
name: null,
price: 0,
type: null, /** phone or book or something else*/
}
});
var Book = Product.extend({
defaults:{
bookAuthor:null,
bookCategory:null
}
});
var ProductView = Backbone.View.extend({
render:function(){}
});
var BookView = ProductView.extend({
render:function(){}
});
var PhoneView = ProductView.extend({
render:function(){}
});
Each sub product owns their attributes and views. With this kind of design, when user change the type of the product by select an option, if I can change the current model to another kind of model, then refresh the view.
But it seems that I can not change a instance of Book to Phone on the fly.
So how do you make the design?
It is good to have separate View and Model. In this problem, we can have FormView, which can contain product view. Instead of changing the model on the fly, every product view can have their model. I would go with this design.
var Product = Backbone.Model.extend({
defaults: {
name: null,
price: 0
}
});
var Book = Product.extend({
defaults:{
bookAuthor:null,
bookCategory:null
}
});
var Phone = Product.extend({
defaults:{
color:null,
weight:null
}
});
var ProductView = Backbone.View.extend({
render:function(){}
});
var BookView = ProductView.extend({
initialize: function(options) {
this.model = new Book(options);
},
render:function(){}
});
var PhoneView = ProductView.extend({
initialize: function(options) {
this.model = new Phone(options);
},
render:function(){}
});
var FormView = Backbone.View.extend({
tagName: 'form',
defaults: {
productView: null
},
events: {
'change select': 'type_change'
},
type_change: function () {
var productType = this.$el.find('select').val()
this.productView.remove();
this.productView = this.getProductView(productType);
this.productView.render();
},
getProductView(product) {
if(product == "book")
return new ProductView();
return new BookView();
}
});
While using backbone to hit an api, I've found that I need to only include some of the data in the response. The webserver is giving me back metadata in addition to data concerning my objects that I don't need.
The following solution works, but doesn't feel right. Is there a standard way of doing this?
var accountsCollection = new AccountsCollection();
accountsCollection.fetch({success : function(collection){
var results = new AccountsCollection();
collection.each(function(item){
results.add(new AccountModel({
id: item.toJSON().result[0].id,
messageText: item.toJSON().messageText,
address1: item.toJSON().result[0].address1,
address2: item.toJSON().result[0].address2
}));
});
onDataHandler(results);
}});
EDIT: This was my final solution based on the accepted answer:
parse: function(response) {
var accounts = [];
_.each(response['result'], function (account) {
accounts.push(account);
});
return accounts;
}
You could try overriding the Backbone.Collection.parse method and do some crazy underscore stuff. No idea if it fits your data..
var keysILike = ['foo', 'bar'];
AccountsCollection.extend({
parse: function(response) {
return _.compact(_.flatten(_.map(response, function (model) {
var tmp = {};
_.each(_.keys(model), function (key) {
if (_.contains(keysILike, key)) tmp[key] = model[key];
})
return tmp;
})));
}
});
With respect to #Sushanth's awesomeness you definitely want to use this solution:
var keysILike = ['foo', 'bar'];
AccountsCollection.extend({
parse: function(response) {
_.each(response, function (model) {
_.each(_.keys(model), function (key) {
if (!_.contains(keysILike, key)) delete model[key]
})
});
return response;
}
});
I have a tastypie API and now I'm playing around with backbone.js . To get both play together nicely I use backbone-tastypie.
This works very well.
Now I want to add offline functionality by using backbone.offline. This are my models and resources in backbone.js:
var Pizza = Backbone.Model.extend({
urlRoot: '/api/v1/pizza/',
});
var Topping = Backbone.Model.extend({
urlRoot: '/api/v1/topping/'
});
var PizzaCollection = Backbone.Collection.extend({
model: Pizza,
url: '/api/v1/pizza/',
initialize: function() {
this.storage = new Offline.Storage('pizza', this);
}
});
var ToppingCollection = Backbone.Collection.extend({
model: Topping,
url: '/api/v1/topping/',
initialize: function() {
this.storage = new Offline.Storage('topping', this);
}
});
Then if I create a collection and do a incremental sync on the stoage object, the request against the API loads normally, but I still have no models in the collection:
var pizzas = new PizzaCollection();
pizzas.storage.sync.incremental();
Can someone help me out here with knowledge about putting together backbone-tastypie and backbone.offline?