Set a Backbone collection model with circular dependencies in requirejs - javascript

The thing is that I have a circular dependecy between some Backbone modules so I have to use "exports" as Requirejs scpecifies in its documentation http://requirejs.org/docs/api.html#circular. So the model 'A' will look like this:
define(function(require, exports) {
var B = require('B');
var A = Backbone.Model.extend({
});
exports.model = A;
});
And the collection 'B' like this:
define(function(require, exports) {
var A = require('A');
var B = Backbone.Model.extend({
model: A.model
});
exports.model = B;
});
The problem here is that by the time I have to specify the collection 'B' model property, the model 'A' isn't yet defined. This is the error I'm getting when I try to set the collection with models like this:
B.collection.set([{id: 1}, {id: 2}]);
Uncaught TypeError: 'undefined' is not an object (evaluating 'targetModel.prototype') (http://127.0.0.1:9999/bower_components/backbone/backbone.js:689)
Any ideas on how should I solve this problem?

From the example, it's not clear that B actually depends on A. If it's just a model:collection relationship, it might make sense to remove the dependency of the model on its collection. If it's at all possible to break the circular dependency, I would strongly encourage you to do so.
If the back-reference is truly required, though, one option might be to move the resources into the same module and do a sort of lazy export:
define(function() {
var lazyThings = {
A: null,
B: null
};
lazyThings.A = Backbone.Model.extend({
collection: things.B
});
lazyThings.B = Backbone.Collection.extend({
model: A
});
return lazyThings;
});
Alternatively, you could return lazyThings.B and later access the model from its prototype:
require('b', function (B) {
var A = B.prototype.model; // A
});
Finally, requirejs could be made to work by calling the respective dependencies lazily (i.e., after the modules are resolved):
// B
define(['a'], function (A) {
return function () {
return Backbone.Collection.extend({
model: A()
});
}
});
// A
define(['b'], function (B) {
return function () {
return Backbone.Model.extend({
model: B()
});
}
});

The following works for me, try to make it clear as possible.
You have a model, you have a collection. In order for them to both depend on each other + avoid a circular dependency, you need a 3rd "mediator" dependency. It's convenient in Backbone to have a model and easily lookup what collection it belongs to, and vice versa, but the problem of course is they have a circular dependency.
So before we had:
+model
+collection
__________
= circular
and after:
+model
+collection
+mediator
________
= OK
//collection
define([
'#allModels',
'#BaseCollection',
'#AppDispatcher',
'#allFluxConstants',
'app/js/flux/flux-helpers/collectionUpdater'
],
function (allModels, BaseCollection, AppDispatcher, allFluxConstants, collUpdater) {
var dispatchCallback = function (payload) {
return true;
};
var BaymaxComponentCollection = BaseCollection.extend({
model: allModels['BaymaxComponent'],
collectionName:'baymax-component',
url: '/baymax_component',
batchURL: '/batch/baymax_component',
initialize: function (models, opts) {
this.dispatchToken = AppDispatcher.register(dispatchCallback);
},
// collection is sorted by original insertion order.
comparator: 'order'
});
return new BaymaxComponentCollection();
});
//model
define([
'#BaseModel',
'#ModelCollectionMediator',
'#AppDispatcher'
],
function ( BaseModel, MCM) {
var BaymaxComponent = BaseModel.extend({
idAttribute: 'id',
urlRoot: '/baymax_component',
collectionName: 'baymax-component',
defaults: function () { //prevents copying default attributes to all instances of UserModel
return {}
},
initialize: function (attributes, opts) {
//*** the following line is crucial ***
this.collection = MCM.findCollectionByName(this.collectionName);
},
validate: function (attr) {
return undefined;
}
},
{ //class properties
});
return BaymaxComponent;
});
//mediator
define(function (require) {
return {
findCollectionByName: function (name) {
var allCollections = require('#allCollections');
return allCollections[name];
}
};
});

Related

JavaScript: Should a function be able to create instances of itself with Object.create()

My use case is the following: I want to create a factory which produces various kinds of data transfer objects (DTOs). They must be easily serializable and they must have a few additional methods.
My current implementation looks like this (simplified):
window.Dto = function(type, properties)
{
var
self = this,
values = {},
object = Object.create(self);
properties.forEach(function(prop){
Object.defineProperty(object, prop, {
get: function() { return values[prop]; },
set: function(value) { values[prop] = value; },
enumerable: true
});
});
this.getType = function()
{
return type;
};
this.doSomeMagic = function()
{
// ...
};
return object;
};
// creating a DTO of the Transport.Motorized.Car class
var carObject = new Dto("Transport.Motorized.Car", ["vendor", "model", "color", "vmax", "price"]);
(Note: I do not want to create an explicit class for each of these objects, because there are hundets of them, and they are exported from the server side. Also, what you see as properties parameter above, is actually a map of meta data with validation constraints etc.)
I did a quick performance check with a loop where 50,000 of such objects were created. performance.now() tells me that it took a bit more than 1s – which looks ok, but not too impressive.
My question is mainly: Is it ok that the factory creates an instance from its own prototype (if I understand correctly what that code does) and returns it? What side effects can it have? Is there a better way?
As far as I understand factory functions, their whole point is not needing to create new instances of the function itself. Instead, it just returns a newly created object.
So instead of using instance properties (via this) of the newly created instance (via the new operator), I would just create an object (let's call it factoryProto) and assign all the "instance" methods to that object instead.
Then, you can use factoryProto as the [[Prototype]] for your new object:
window.Dto = function(type, properties) {
var factoryProto = {
getType: function() {
return type;
},
doSomeMagic: function() {
// ...
}
},
values = {},
object = Object.create(factoryProto);
properties.forEach(function(prop) {
Object.defineProperty(object, prop, {
get: function() { return values[prop]; },
set: function(value) { values[prop] = value; },
enumerable: true
});
});
return object;
};
// creating a DTO of the Transport.Motorized.Car class
var carObject = Dto("Transport.Motorized.Car", ["vendor", "model", "color", "vmax", "price"]);
If you want to fully profit from the prototype-chain, you could define the factoryProto outside of the factory function. To keep track of type, you could add it as a non-enumerable object property:
window.Dto = (function() {
var factoryProto = {
getType: function() {
return this.type;
},
doSomeMagic: function() {
// ...
}
};
return function(type, properties) {
var values = {},
object = Object.create(factoryProto);
properties.forEach(function(prop) {
Object.defineProperty(object, prop, {
get: function() { return values[prop]; },
set: function(value) { values[prop] = value; },
enumerable: true
});
});
Object.defineProperty(object, 'type', {
value: type,
enumerable: false
});
return object;
};
})();
// creating a DTO of the Transport.Motorized.Car class
var carObject = Dto("Transport.Motorized.Car", ["vendor", "model", "color", "vmax", "price"]);

Is there a more elegant way to "fake" class inheritance?

I have not found an easy way to extend Mongoose Schema/Model methods because of the way that mongoose handles them, and because of the fact that mongoose=require('mongoose') is a singelton.
So, I am 'faking' class inheritance here:
'use strict';
var _ = require('lodash');
module.exports = function(MongooseModel, options) {
var Collection = {};
_.assign(Collection, _.toPlainObject(MongooseModel));
Collection.pluralName = Collection.modelName + 's';
Collection.foo = Collection.bar;
return Collection
};
Does anyone have a more elegant solution?
EDIT:
Turns out the above solution doesn't work. For instance, using Collection.find({}, function(err, docs) {...}) will error when Mongo tries to create "docs" from a model that has not been registered with Mongoose.
So, what I've done is now completely inelegant:
'use strict';
var _ = require('lodash');
module.exports = function(MongooseModel, options) {
var Collection = MongooseModel;
...
return Collection
};
There are some ways to try and do this, though not sure exactly what your trying to extend.
You can add instance methods <schema>.methods.<mymethod> = function(){}
// define a schema
var animalSchema = new Schema({ name: String, type: String });
// assign a function to the "methods" object of our animalSchema
animalSchema.methods.findSimilarTypes = function (cb) {
return this.model('Animal').find({ type: this.type }, cb);
}
And you can add static methods <schema>.statics.<mymethod> = function(){}
// assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function (name, cb) {
return this.find({ name: new RegExp(name, 'i') }, cb);
}
var Animal = mongoose.model('Animal', animalSchema);
Animal.findByName('fido', function (err, animals) {
console.log(animals);
});
Examples are from the mongoose docs - just search for "statics".
The statics functions you can call on a model. The methods are usually functions that work with an instance of a document returned from a query or created with new.

Sails.js Access Model on Service initialization

The question:
As I understand in sails.js during initialization process Services are initialized before Models.
Is there any possibility to change this behavior? To make Models load before Services.
If it's not, then how can I load particular settings from the database to use them to build instance of my class described in some Service during this Service initialization?
A little bit code for solidity:
api/models/Model.js
console.log("Model Identified");
module.exports = {
attributes: {
name: { type: 'string', required: true, size: 15 },
//Some extra secret fields
}
};
...
api/services/MyCoolService.js
console.log('service inits');
function MyCoolService(options){
//some extraordinary constructor logic may be ommited
}
MyCoolService.prototype.setOptions = function(options){
//Set values for MyCoolService fields.
}
//Some other methods
var myCoolServiceWithSettingsFromDb = new MyCoolService();
//That's the place
model.findOne(sails.config.myApplication.settingsId).exec(function(err,result){
if(!err)
myCoolServiceWithSettingsFromDb.setOptions(result);
});
module.exports = myCoolServiceWithSettingsFromDb;
It's because you instantiate object in service with constructor that needs sails that not exist. Try use this at MyCoolService;
module.exports = {
someOption: null,
method: function () {
var that = this;
sails.models.model.findOne(sails.config.myApplication.settingsId)
.exec(function (err, result) {
if (!err)
that.someOption = result;
});
}
};
that method can be called by sails.services.mycoolservice.method() or simply MyCoolService.method() to give your service some option from DB.
If you want to initiate them at Sails start, call that method at config/bootstrap.js
Thanks to Andi Nugroho Dirgantara,
I ended up with this solution (I still don't like it much, but it works):
api/services/MyCoolService.js
console.log('service inits');
function MyCoolService(options){
//some extraordinary constructor logic may be ommited
}
//All the same as in question
//The instance
var instance;
module.exports = module.exports = {
init: function(options) {
instance = new MyCoolService(options);
},
get: function() {
return instance;
},
constructor: MyCoolService
};
config/bootstrap.js
...
Model.findOrCreate({ id: 1 }, sails.config.someDefaultSettings).exec(function(err, result) {
if (err)
return sails.log.error(err);
result = result || sails.config.someDefaultSettings;
MyCoolService.init(result);
return sails.log.verbose("MyCoolService Created: ", TbcPaymentProcessorService.get());
});
...
tests/unit/service/MyCoolService.test.js
...
describe('MyCoolService', function() {
it('check MyCoolService', function(done) {
assert.notDeepEqual(MyCoolService.get(), sails.config.someDefaultSettings);
done();
});
});
...
It works: the service is instantiated once while bootstraping and it's instance is avaliable everywhere.
But to me this solution still weird... I still don't understand how to globally instantiate instance of my service (for use in a lot of controllers) and make it the best way.

Why is the new operator used for collections in todomvc dependency example

I am reading through the todomvc Backbone dependency example and noticed that the 'new' operator is used to create new collections, however the views, models, and routers, return the object itself.
Why is the new operator required for collections?
Collection
/*global define */
define([
'underscore',
'backbone',
'backboneLocalstorage',
'models/todo'
], function (_, Backbone, Store, Todo) {
'use strict';
var TodosCollection = Backbone.Collection.extend({
// Reference to this collection's model.
model: Todo,
// Save all of the todo items under the `"todos"` namespace.
localStorage: new Store('todos-backbone'),
// Filter down the list of all todo items that are finished.
completed: function () {
return this.where({completed: true});
},
// Filter down the list to only todo items that are still not finished.
remaining: function () {
return this.where({completed: false});
},
// We keep the Todos in sequential order, despite being saved by unordered
// GUID in the database. This generates the next order number for new items.
nextOrder: function () {
return this.length ? this.last().get('order') + 1 : 1;
},
// Todos are sorted by their original insertion order.
comparator: 'order'
});
return new TodosCollection();
});
Model
/*global define*/
define([
'underscore',
'backbone'
], function (_, Backbone) {
'use strict';
var Todo = Backbone.Model.extend({
// Default attributes for the todo
// and ensure that each todo created has `title` and `completed` keys.
defaults: {
title: '',
completed: false
},
// Toggle the `completed` state of this todo item.
toggle: function () {
this.save({
completed: !this.get('completed')
});
}
});
return Todo;
});
When a module returns a new Object() it is considered a Singleton since caching would not allow the same module to get loaded again.
The collection object I'm assuming you are looking at is therefore a Singleton. It is already instantiated so you would not need to do that.
This is the file I'm assuming you are questioning about.
Also, it isn't necessarily required it is just an architectural decision. Here is a great article about exporting in modules.

Is this a best practice to cache request data in Backbone.js?

I have a requestCache: {} object in my router declaration. I have a reviews method mapped to a route with the same name (#reviews). I want to cache the results generated inside this method.
router.js
var AppRouter = Backbone.Router.extend({
currentView: null,
requestCache: {},
reviews: function() {
var self = this;
var reviewCollection = new ReviewCollection();
reviewCollection.url = '/profile/' + this.userid + '/reviews';
if('reviews' in self.requestCache) {
reviewCollection = self.requestCache['reviews'];
self.changeView(new ReviewsView({collection:reviewCollection}), 'reviews');
} else {
reviewCollection.fetch().done(function() {
self.requestCache['reviews'] = reviewCollection;
self.changeView(new ReviewsView({collection:reviewCollection}), 'reviews');
});
}
},
changeView just renders the view using the results.
This works fine. What I want to know is whether this is a good way of caching data?
Take a look at backbone-fetch-cache. It does what you want.
As suggested by SoundCloud team, they've craeted a store object to share models and collections through the code.
I've been using Backbone SingletonModel (https://github.com/reconbot/backbone-singleton)
It works just fine and you can make the same for your collections, defining a getInstance method and a _instance on its static part.
var MyCollection = Backbone.Collection.extend({}, {
_instance: null,
count: 0,
getInstance: function () {
if (!this._instance)
this._instance = new MyCollection();
this.count++;
return this._instance;
}
});

Categories

Resources