What's the difference between using _.extend({}, Backbone.Events) and _.clone(Backbone.Events) for an event aggregator? I have seen them both used for this purpose:
http://backbonejs.org/#Events
http://lostechies.com/derickbailey/2011/07/19/references-routing-and-the-event-aggregator-coordinating-views-in-backbone-js/
There is absolutely no difference. The definition of underscore's clone method is:
_.clone = function(obj) {
if (!_.isObject(obj)) return obj;
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};
So if the argument to _.clone is an object, the cloning is done via:
_.extend({}, obj);
Using the _.extend({}, Backbone.Events) syntax makes sense, when you also want to define other properties on the new object. Because extend accepts any amount of arguments, each of which will be extended into the first argument, it is common to define evented objects as follows:
//define an evented object
var foo = _.extend({}, Backbone.Events, {
bar: function() { /*...*/ }
});
Related
My code is:
class myclass observable.Observable
{
let label = "test";
navigatingTo(args: observable.EventData)
{
target.on( "name", this._callback );
}
_callback ( eventData )
{
console.log( this.label);
}
}
When I print out this.label in the callback - "this" object is not the object that I expect - which I think should be the myclass instance.
I've got a separate method for the callback because I'm also calling .off() later and need a reference to the method (as opposed to anonymous function)
You can pass a third argument when subscribing with on(). The third argument will be used as a context(this) for the callback. So probably you want to do:
target.on("name", this._callback, this);
Let's say I have a map:
map = new can.Map({foo: 'bar'})
and I want to bind the value of foo in another map. I could do:
otherMap = new can.Map({fizzle: map.compute('foo')})
but this doesn't behave the way I would expect.
I would expect otherMap.attr('fizzle') to return bar, but instead it returns a function. I have to call otherMap.attr('fizzle')() instead.
I would expect to be able to change the value by calling otherMap.attr('fizzle', 'moo'), but this doesn't change the computed value. Instead, if I want to change the underlying value, I have to call otherMap.attr('fizzle')('moo').
Is there a way to create a map with computed values that behave like normal attributes?
Thanks!
I would recommend using the define plugin which makes it easy to create computed getters and setters without having to explicitly create computes. In your example like this:
var map = new can.Map({
foo: 'bar',
baz: 'bla'
});
var OtherMap = can.Map.extend({
define: {
fizzle: {
get: function() {
return map.attr('foo') + '/' + map.attr('baz');
},
set: function(value) {
map.attr('foo', value);
}
}
} });
var other = new OtherMap();
console.log(other.attr('fizzle'));
other.attr('fizzle', 'something');
console.log(map.attr('foo'));
Demo in this Fiddle.
I want to implement "smart" caching in my application. I want to always first return data from the cache (if none is available an empty object/array is returned), then always fetch the data from the server and replace the cached response with the updated server response. The objective is to always quickly show something to the user.
I want to do it in an "angular" fashion, i.e - adher to the promise paradigm.
I found a solution that uses the $resource service (http://www.bennadel.com/blog/2432-Applying-A-Cached-Response-To-An-AngularJS-Resource.htm), but $resource pretty much sucks if you don't use only the 4-5 default REST methods that it offers. Its custom method functionality is severly lacking. I'd really like to use the low level $http service, since it gives me better control over my requests, while keeping my controllers oblivious to the whole caching functionlity (i.e - avoid fetching data from the cache first in the controller itself and then querying the service).
Has anyone dealt with this problem and has a better solution?
Thanks :-)
http://jsfiddle.net/G23h7/
I created two services to accomplish what I think you're trying to accomplish. The first provides the core functionality to take in a promise and an object (or array) and updates said object or array when the promise resolves. There's a GUI for you to play around with it.
The second service integrates that into $http. Basically you can do smartHttp.forArray(config) and smartHttp.forObj(config) in lieu of $http(config). If you end up using this and want to use the $http shortcut methods then that should be straightforward to implement. This is untested - so consider it as pseudocode. If you're instantly returning a cached value/dud value it doesn't really make sense to use a promise for the return value of your smartHttp service (unless you were trying to make the service interchangeable with $http). If you'd like it to be a promise for that or whatever reason you can change:
var general = function (obj, methodName) {
// ...
return obj;
};
to the following:
var general = function (obj, methodName) {
// ...
return $q.when(obj);
};
And then ask for the $q service, of course. The real issue here is equality between requests - I assume $http does that nicely; I made a naive key - you may want to change that (as long as you have simple requests/same order for everything I don't think it should matter).
myApp.factory('smartCache', function () {
var service = {};
service.forArray = function (array, promise, clear) {
promise.then(function (promiseResult) {
if (clear) {
array.length = 0;
}
angular.forEach(promiseResult, function (promiseResultElement) {
array.push(promiseResultElement);
});
});
};
service.forObj = function (obj, promise, clear) {
promise.then(function (promiseResult) {
if (clear) {
for (var prop in obj) {
delete obj[prop];
}
}
for (var prop in promiseResult) {
obj[prop] = promiseResult[prop];
}
});
};
return service;
});
myApp.factory('smartHttp', function ($http, smartCache, $cacheFactory) {
var cache = $cacheFactory('smartHttp');
var service = {};
var general = function (config, methodName, initialValue) {
var obj;
var key = JSON.stringify([ config.url, config.method, config.params, config.data ]);
var cachedObj = cache.get(key);
if (cachedObj !== undefined) {
obj = cachedObj;
} else {
obj = initialValue;
}
var promise = $http(config);
var smartCachePromise = promise.then(function (result) {
return result.data;
});
smartCache[methodName](obj, smartCachePromise, true);
return obj;
};
service.forObj = function (config) {
return general(config, 'forObj', {});
}
service.forArray = function (config) {
return general(config, 'forArray', []);
}
return service;
});
I have a backbone collection on the client.
Model of the collection has some properties along with another collection
When I do fetch() my action method on the server returns some data, collection gets populated, all the properties too, except that nested collection.
What could be the reason?
var Job = Backbone.Model.extend();
var Jobs = Backbone.Collection.extend({model: Job})
var Foo = Backbone.Model.extend({
initialize:function(){
this.jobs = new Jobs();
}})
var FooCollection = Backbone.Collection.extend({model: Foo})
var fooCol = new FooCollection()
fooCol.fetch();
fooCol.first().get('name') // => returns name
fooCol.first().jobs.toJSON() // returns nothing
// although this will
fooCol.first().get('jobs') //it will return an array
So somehow nested Backbone collection becomes just a regular property (Array)
OK - with your extra information, I can give you an answer.
First - "get" doesn't get a property off of the model. It gets a property off of the model's attributes property. So, the attributes probably look like:
{
name: 'blah',
jobs: [{name: 'job1'}, {name: 'job2'}]
}
Backbone doesn't automagically transform arrays into collections and models, and simply setting this.jobs isn't going to work. What you need to do is a little more complex.
var Foo = Backbone.Model.extend({
initialize:function(){
this.jobs = new Jobs(this.attributes.jobs));
}
});
This will set your 'jobs' property to a new jobs object with the data that was sent over for the jobs. But, alas, it won't automatically fire events on the Jobs collection, nor will it allow you to use helpers like this.get('jobs').each(fn); - you'll only be able to use it as Foo.jobs.each(fn).
In order for you to use the attribute as an actual collection, you'll have to do a lot more complicated things.
var Foo = Backbone.Model.extend({
initialize:function(){
this.createJobs(this.attributes.jobs);
},
toJSON: function () {
var json = Backbone.Model.prototype.toJSON.apply(this);
json.jobs = this.get('jobs').toJSON();
return json;
},
set: function (key, val) {
var attributes;
if(!_.isObject(key)) {
attributes = {}; attributes[key] = val;
} else {
attributes = key;
}
safeAttributes = _.omit(attributes, 'jobs');
Backbone.Model.prototype.set.call(this, safeAttributes);
if(attributes.jobs) { this.get('jobs').reset(attributes.jobs); }
},
clear: function () {
if(this.get('jobs') && this.get('jobs').destroy) {
this.get('jobs').off();
this.get('jobs').destroy();
}
Backbone.Model.prototype.clear.apply(this);
this.createJobs();
},
createJobs: function (jobsArray) {
var jobsCollection = new Jobs(jobsArray);
jobsCollection.on('change', function () {this.trigger('change'); }, this);
this.set('jobs', jobsCollection);
}
});
Note that this is completely untested, but hopefully it shows some of the way you'd do this.
I have the following backbone.js code. I'm using an object literal for organizing my code, which has left me with a question regarding the best way to proceed. The application (in its simplified form below) has a control panel (which can be shown or hidden) which is used to add new categories to a collection. (Question follows)
(function($){
// ============================= NAMESPACE ========================================
var categoryManager = categoryManager || {};
// ============================= APPLICATION =================================================
categoryManager.app = categoryManager.app || {
/* Used to Initialise application*/
init: function(){
//this.addView = new this.addCategoryView({el: $("#add-new-category")})
//this.collection = new this.categoryCollection();
new this.addCategoryView({el: $("#add-new-category")})
new this.categoryCollection();
},
categoryModel: Backbone.Model.extend({
name: null
}),
addCategoryView: Backbone.View.extend({
events: {
"click #add-new-category-button.add" : "showPanel",
"click #add-new-category-button.cancel" : "hidePanel",
"click #new-category-save-category" : "addCategory"
},
showPanel: function() {
$('#add-new-category-button').toggleClass('add').toggleClass('cancel');
$('#add-new-category-panel').slideDown('fast');
},
hidePanel: function() {
$('#add-new-category-button').toggleClass('add').toggleClass('cancel');
$('#add-new-category-panel').stop().slideUp('fast');
},
addCategory: function() {
//categoryManager.app.collection.create({
categoryManager.app.categoryCollection.create({ // My Problem is with this line
name: $('#name').val()
});
}
}),
categoryCollection: Backbone.Collection.extend({
model: this.categoryModel,
initialize: function () {
}
})
}
// ============================= END APPLICATION =============================================
/* init Backbone */
categoryManager.app.init();
})(jQuery);
Now obviously the problem with the above, is that calling the addCategory function tries to call a function on an object which is uninitialized. I've worked round the problem (see commented out code) by calling the function instead on a object which is instantiated within the init function. My question is - is this the right thing to do? I detect a code smell. I feel that the contents of the object literal shouldn't rely on the object being created in order to be valid. the function addCategory in this instance wouldn't work unless the init function had been called on the parent first. Is there another pattern here that I should be using?
How else would I pass the contents of the 'create new category form' to the collection in order to be added (I'm using create because I want to automatically validate/create/persist the model and It seems like the easiest thing to do). I'm a rock bottom novice with backbone (this is my 'hello world')
Thanks
I think the main issue is you are treating categoryCollection as if it's an object. It's not really an object, but a constructor function. So first you need to create an instance, as you have discovered.
Then the addCategoryView needs some way of referencing the instance. It looks like you don't have a model associated with the view. I would suggest creating a model and storing the categoryCollection instance as a property of the model. Something like this (warning, untested code):
var model = new BackBone.Model({
categories: new categoryManager.app.CategoryCollection()
});
var view = new categoryManager.app.AddCategoryView({
el: $("#add-new-category"),
model: model
});
Then you can just use this.model.categories from inside addCategoryView.
As an aside, a common Javascript convention is to capitalize the names of constructors. Calling the constructor CategoryCollection might make the code a little bit clearer.
You need to initialize collection before create a new instance of a model
addCategory: function() {
var collection = categoryManager.app.categoryCollection;
!collection.create && (collection = new collection);
collection.create({
name: $('#name').val()
});
}