jQuery like event maps for Backbone listenTo? - backbone.js

I have a view which needs to listenTo the change of a model attribute. When that change event fires, I have to call two different callback functions. I know this can be easily accomplished using .on like
model.on({
'change:title' : updateTitle,
'change:title' : updateSummary
});
But when using listenTo I have to write this in multiple lines
this.listenTo(model, 'change:title', updateTitle);
this.listenTo(model, 'change:title', updateSummary);
Is there a way to avoid having to re-write the listenTo statement multiple times?
Or does this issue mean that my code isn't structured properly and I should rethink my approach?
A simple alternative is that I call updateSummary from updateTitle, but I was just wondering if this can be accomplished using object.listenTo

does this issue mean that my code isn't structured properly and I should rethink my approach?
I don't see any problem having two (or more) listeners registered to the same event. That's a core point of decoupling. You could have different (decoupled) Views that update their contents when the same event is triggered.
I don't know the rest of your code, but I suppose your updateSummary listens to other change events apart from the change:title:
this.listenTo(model, 'change:summary', updateSummary);
In this case you are registering the same listener (updateSummary) to different events (change:title and change:summary). And that's ok, with listenTo you are sort of connecting different view updates to different model events in a decoupled way.
Is there a way to avoid having to re-write the listenTo statement multiple times?
Maybe an overkill, but you could use the functions that underscore provides to have something like this:
_.each([updateTitle, updateSummary], _.partial(this.listenTo, model, 'change:title'), this);
A simple alternative is that I call updateSummary from updateTitle, but I was just wondering if this can be accomplished using object.listenTo
I'm afraid that listenTo only accepts a callback function, not an array of functions.
If you are still worried about having listenTo on the same event multiple times, you can create a more general function, let's say update, that updates both title and summary, and register only this more general update function:
this.listenTo(model, 'change:title', update);
or if you don't want another function, you can use an anonymous one:
this.listenTo(model, 'change:title', function() {updateTitle(); updateSummary();});

As #muistooshort already mentioned in the comment :
var objParam={
'change:title' : updateTitle,
'change:title' : updateSummary
};
model.on(objParam);
See carefully, objParam has 2 keys with the same name, this will give error in strict mode, otherwise will just override the previous value of the key & hence, console.log(objParam) prints
{ 'change:title' : updateSummary }
and this is what passed as argument to the .on call.

Related

Angularjs ui-route correct implementation for multiple views

I would like to implement a page like this
I have the following folder structure:
-root/
--root.controller.js
--root.module.js
--root.routes.js
--root.html
--filters/
----filters.controller.js
----filters.module.js
----filters.routes.js
----filters.html
--tableData/
----tableData.controller.js
----tableData.module.js
----tableData.routes.js
----tableData.html
--graph/
----graph.controller.js
----graph.module.js
----graph.routes.js
----graph.html
I would like to know the best approach in order to manage call from each controller in the best way.
I mean, from filters, I can change selection on dropdown or update other fields values, and then I would update data on tabledata. And if I change selection on tabledata I would update graph.
Root url is '/root/{itemId}' and I cannot add some other value on querystring.
How can I manage internal variables and methods?
thanks a lot
From my understanding, it's better to go for events $emit , $broadcast & on to achieve the same.
As I can see, you are having independent controllers with no relationship with most of the the other controllers.
Few suggestions to implement it:
use $rootScope.$emit and $rootScope.on if they controllers have no relationship. Make sure you are removing it manually though (can be ignored if the app is not too heavy but should be kept in mind). This link will be surely helpful . Eg:
If the filter is changed, an event filter-changed will be triggered. The graph.controller and table.controller will be informed and will render the UI accordingly.
Create a messaging service from where the controllers will subscribe and unsubscribe the events. That will keep numbers of events in check. You'll be aware of how many events have actually been created in the application.

Unable to wrap functions assigned with events parameter in backbone model

Motivation: I want to wrap all functions within backbone model with log functionality to make it more easy to follow function calls.
I am iterating over models functions and warping them with underscores wrap().
But it looks like event function bindings from 'events' hash doesn't get wrapped as it looks like these functions are copied and stored somewhere in DOM, not sure.
Has anybody came to this problem as well or any ideas how to workaround?
Manually entering console.log() in these function is not what I am looking for.
I think you're experiencing the same problem as when people are trying to test the functionality provided with the events -hash. Spying on the event callback directly on the View doesn't work, but spying on the class prototype before instantiating works.
// This won't work
var view = new SomeView();
spyOn(view, 'onClick');
view.$el.click(); // spy isn't called
// This works
spyOn(SomeView.prototype, 'onClick');
var view = new SomeView();
view.$el.click(); // spy is called
Try iterating over the prototype of your 'class' right after creating it, because that should be where the methods called by the events -implementation should reside.

Test Driving Backbone view events

I am trying to test drive a view event using Jasmine and the problem is probably best explained via code.
The view looks like:
App.testView = Backbone.View.extend({
events: { 'click .overlay': 'myEvent' },
myEvent: function(e) {
console.log('hello world')
}
The test looks something like:
describe('myEvent', function() {
it('should do something', function() {
var view = new App.testView();
view.myEvent();
// assertion will follow
});
});
The problem is that the view.myEvent method is never called (nothing logs to the console). I was trying to avoid triggering from the DOM. Has anyone had similar problems?
(Like I commented in the question, your code looks fine and should work. Your problem is not in the code you posted. If you can expand your code samples and give more info, we can take another look at it. What follows is more general advice on testing Backbone views.)
Calling the event handler function like you do is a legitimate testing strategy, but it has a couple of shortcomings.
It doesn't test that the events are wired up correctly. What you're testing is that the callback does what it's supposed to, but it doesn't test that the action is actually triggered when your user interacts with the page.
If your event handler needs to reference the event argument or the test will not work.
I prefer to test my views all the way from the event:
var view = new View().render();
view.$('.overlay').click();
expect(...).toEqual(...);
Like you said, it's generally not advisable to manipulate DOM in your tests, so this way of testing views requires that view.render does not attach anything to the DOM.
The best way to achieve this is leave the DOM manipulation to the code that's responsible for initializing the view. If you don't set an el property to the view (either in the View.extend definition or in the view constructor), Backbone will create a new, detached DOM node as view.el. This element works just like an attached node - you can manipulate its contents and trigger events on it.
So instead of...
View.extend({el: '#container'});
...or...
new View({el:'#container'});
...you should initialize your views as follows:
var view = new View();
$("#container").html(view.render().el);
Defining your views like this has multiple benefits:
Enables testing views fully without attaching them to DOM.
The views become reusable, you can create multiple instances and render them to different elements.
If your render method does some complicated DOM manipulation, it's faster to perform it on an detached node.
From a responsibility point of view you could argue that a view shouldn't know where it's placed, in the same way a model should not know what collection it should be added to. This enforces better design of view composition.
IMHO, this view rendering pattern is a general best practice, not just a testing-related special case.

backbone model with an array/object property: infinite 'change' event triggered after sync()?

My backbone.js model has an array property. I bound the change event to save().
After sync() (triggered by save(), my app server returns an identical JSON, but backbone thinks the array has been changed (due to a different reference to the array I guess?), and trigger changes again. Then an infinite loop occurs.
save() -> sync() -> triggered `change` -> save()...
What shall I do?
Idea: I can bind the change event to a function that checks if the changed attributes are of type object/array, and do a deep comparison and call save only if the array/object really changed. If true then save()?
Thanks!
Try the Edge version of Backbone (master branch) this behavior changed after 0.9.9 - see https://github.com/documentcloud/backbone/pull/2004
Backbone has a special option on many methods to prevent just this sort of issue: silent:true. If you pass that option to your save method, the resulting sync won't trigger a change event.
So, if you want to set your change event handler to save silently, something like:
changeHandler: function() {
this.save({silent:true});
}
should do the trick.

Binding for "change" in backbone model not working

Here's the Example
I was following this excellent tutorial by Thomas Davis : What is a model?
Somehow the 'change' binding is not firing. What am I doing wrong here?
Backbone is checking if the set value is the same as the previous value (look at https://github.com/documentcloud/backbone/blob/master/backbone.js#L210 and on).
In your example, the array is still the same but the value inside changed. This is tricky to solve. Creating a new copy of the array seems to be overhead. I would suggest to call the change event directly in your adopt function as a solution:
adopt: function(newChildsName){
var children_array = this.get('children');
children_array.push(newChildsName);
this.set({children:children_array});
this.trigger("change:children");
}
I would suggest to create an issue on backbone github repository to maybe add a "force" option to force the update (thus triggering the event) of attributes on a model.
Here is a bit awkward solution:
adopt: function(newChildsName){
var children_array = this.get('children').splice(0);
children_array.push(newChildsName);
this.set({children:children_array});
}
Instead of using children as an plain array we can use it as an collection and listen to the add,remove events of the collection.

Resources