Is it possible to listen for a change in a model in a collection if a specific field is changed to a specific value?
I know that something like 'change:fieldName' exists, I'm looking for something like 'changeTo: fieldName = true'
There's not a "shortcut" way of doing so. You have to listen to the normal change event, and in your listener, see if the value has changed to something interesting for you. Then, propagate the event, fire a new one, or do stuff.
Backbone.Collection.extend({
initialize: function() {
this.on('change:property', this.onChange);
},
onChange: function(e) {
// sorry for pseudo-code, can't remember syntax by heart, will edit
if (e.newValue == true)
myLogic();
}
}
You cannot listen for an explicit value since that wouldn't work well in the general case, but you can easily bind to the general handler and run your code based on that.
var MyCollection = Backbone.Collection.extend({
initialize: function(models, options){
this.on('change:myProperty', this.changeMyProperty_, this);
},
changeMyProperty_: function(model, value){
if (value) this.myPropertyTrue_(model);
},
myPropertyTrue_: function(model){
// Do your logic
}
});
Related
In the following code,
this.listenTo(this.model, 'change:voteCount', this.changeVoteCount);
this.listenTo(this.model, 'change', this.renderTemplate);
If "voteCount" attribute updated, I want to trigger "this.changeVoteCount" method but not trigger "this.renderTemplate".
I already googling about 2 hours and couldn't find solution. How can I do for this requirement?
Update
(just the explanation for why I choose #try-catch-finally answer)
For people who also meet this problem, I want to clear out the confusion which answer should be chosen as their solution.
I will describe some details to explain why I choose this answer. The following is my model structure:
{
'name': 'some string',
'location': 'some string',
'link': 'some string',
'voteCount': 0 }
Otherwise 'voteCount' attribute updated, I want to trigger "change" event binded method (in my case, this.renderTemplate).
So I tried the first approach,
this.listenTo(this.model, 'change:name change:location change:link', this.renderTemplate);
In this approach, if I update 'location' and 'name' at once in one place, then the this.renderTemplate method will trigger two times. I want this.renderTemplate method trigger just one time whenever 'location', 'name' or 'link' attributes updated separately or together at the same time.
So finally, I tried the following approach and succeed.
this.listenTo(this.model, 'change', function(model) {
if (_.isEmpty(_.intersection(_.keys(model.changed), ["voteCount"]))) {
this.renderTemplate.apply(this, arguments);
}
});
Beside listing all attributes you want to listen for their change mentioned by #antejan, you could do this programatically too.
This way you'd examine the model's changed attribute which contains all attributes that have been changed.
this.listenTo(this.model, 'change', function(model) {
if (!_.contains(_.keys(model.changed), "voteCount") {
this.renderTemplate.apply(this, arguments);
}
});
When blacklisting multiple keys you could also do:
if (_.isEmpty(_.intersection(_.keys(model.changed), ["attr1", "attr2", ...])) { ...
As long as your model does only have a few attributes I'd prefer #antejans way of listing all attributes explicitly since its more clear whats going on:
this.listenTo(this.model, "change:attr1 change:attr2 ...", this.renderTemplate);
Note that there might be a performance penalty when listing multiple change:attr events since Backbone will call the listeners in a for-loop. See the annotated Backbone source of set():
...
if (!silent) {
if (changes.length) this._pending = options;
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing) return this;
if (!silent) {
while (this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
If you don't want render on every parameter change, you can specify which of them should trigger render.
this.listenTo(this.model, 'change:param1 change:param2', this.renderTemplate);
Edit: if you don't want to render multiple times on multiple fields change, you can use underscore's debounce:
this.listenTo(this.model, 'change:param1 change:param2', _.debounce(this.renderTemplate));
It will call render only once if it will be called several times in a short time.
I have an observable, say myValue. This observable has some properties, like id, also observable. I defined a custom binding to take care of myValue alterations.
When I change the value of the observable property id, the custom binding for myValue is triggered.
This is higly undesirable, because update function in my custom binding is very expensive and I experience performance issues.
Is there a way to prevent parent observables to be updated along with its unpdated child?
The core parts of code
//model opening, declaration, initialize...
//In the model
var self = this;
this.myValue = ko.observable();
this.myValue("Some value");
this.myValue.id = 123;
this.changeId = function() {
self.myValue.id(111); //here the update for myValue is triggered
}
//After the model
ko.bindingHandlers.customBinding = {
init: function init(element, valueAccessor, allBindingsAccessor)
{/*nothing*/},
update: function(element, valueAccessor, allBindingsAccessor) {
/*expensive code*/
console.log("I am being executed every time myValue.id() is changed...");
}
}
//Then the model is applied...
ko.applyBindings(model);
EDIT: I imply if there is a way to alter myValue.id without removing its observable nature. Declaring myValue.id not observable could solve the problem, but I'd need it to be observable, so I would use this solution only as last change.
Anyway, would declaring myValue.id not observable solve the problem? I didn't checked it yet
If I understand correctly, you could try something like this:
var myValue = ko.observable();
myValue.attributes = { id: ko.observable() };
Then a change to myValue.attributes.id should not bubble up to myValue since there is a non-observable object between the two, but I haven't tested it.
EDIT
Sorry to hear that this did not work. My last idea, and this is an ugly hack, would be to throttle the observable update.
var myValue = ko.observable();
myValue.id = ko.observable()
.extend({ rateLimit: { timeout: Integer.MAX_VALUE, method: "notifyWhenChangesStop" } });
This waits for Integer.MAX_VALUEms each time the id has changed before propagating the change and resets when there is a new change in that time. In a real world solution, this should effectively disable the event propagation, but it is, as I said, a hack. Maybe a custom extender could be written in much the same way as the rateLimit one which would do the desired thing, but I don't know the KO source.
In Backbone, when I set the attribute of a Model to the same value it had before, "change" events do not trigger.
My View:
initialize: function() {
this.$lastRollResult = this.$el.find('#lastRollResult');
this.listenTo(this.model, 'change:lastRoll', this.render);
},
render: function() {
this.$lastRollResult
.html(this.model.get('lastRoll'))
.addClass('shaking');
var self = this;
setTimeout(function() {
self.$lastRollResult.removeClass('shaking');
}, 1000);
},
events: {
'click #button': function() {
this.model.set('lastRoll', getRandomInteger(1, 6));
}
}
Sometimes when I call getRandomInteger(), the int will be the same as the previous one, causing my .set('lastRoll', int) to not register as a "change" event. But this means my "shaking" class won't be applied, and that is bad.
How can I make Backbone always recognize when an attribute is set, even if the value I set was the same? Is there some sort of "set" event? (Didn't see one in the docs.)
Edit: I'm aware that there is probably a {silent: true} / .trigger() hack to accomplish this, but is there any "clean" or "semantic" way to always recognize a .set() call?
I think your design is wrong. The view shouldn't be picking a random number and tell the model that that's its new value, the view should be telling the model to roll up a new number. Then you could have this in your model:
roll: function() {
this.set('lastRoll', getRandomInteger(1, 6), { silent: true });
this.trigger('change:lastRoll', this, this.get('lastRoll'));
}
and your view would simply this.model.roll(). Yes, you'd still be doing the {silent:true} and trigger hack but at least it would be in the right place.
Of course, this could have odd side effects so you could go behind the model's back and quietly unset the value before setting a new one:
roll: function() {
// I suppose you could quietly unset here if you feel bad
// about fiddling with `attributes`.
this.attributes.lastRoll = null;
this.set('lastRoll', getRandomInteger(1, 6));
}
Again, having a separate roll method on the model is a handy way to hide all this chicanery from the outside world.
I'm looking for a simple event aggregator that works with require.js. I have two modules, one containing a view model and another with a "listener" of some sort:
// view model
define(['lib/knockout-2.2.1', 'events/aggregator'], function(ko, events){
var squareViewModel = function(contents) {
this.click = function(){
events.publish('squareClicked', this);
};
};
return squareViewModel;
});
// some listener of some kind
define(['events/aggregator'], function(events){
events.subscribe('squareClicked', function(e){
alert("hurrah for events");
});
});
Is there anything out there that does this? Is this kind of architecture even a good idea? This is my first foray into client-side architecture.
This is similar to what you posted, but I've had good luck with extending Backbone events (you don't actually have to use anything else about Backbone for this to work), something like this:
define(['underscore', 'backbone'], function( _, Backbone ) {
var _events = _.extend({}, Backbone.Events);
return _events;
});
And then all your viewmodels (or any code really) can use it:
define(['knockout', 'myeventbus'], function(ko, eventBus){
return function viewModel() {
eventBus.on('someeventname', function(newValue) {
// do something
});
this.someKOevent = function() {
eventBus.trigger('someotherevent', 'some data');
};
};
});
The original idea came from this article by Derick Bailey. One of my other favorites on client-side events is this one by Jim Cowart
Again, it amounts to nearly the same thing as what you posted. However, the one thing I don't like about the approach of tying it to jQuery document DOM node is that you might have other types of events flying through there as well, bubbled up from child nodes, etc. Whereas by extended backbone events you can have your own dedicated event bus (or even more than one if you e.g. wanted to have separate data events vs. UI events).
Note regarding RequireJS in this example: Backbone and Underscore are not AMD-compatible, so you'll need to load them using a shim config.
I ended up using jQuery to make my own:
define([], function(){
return {
publish: function (type, params){
$(document.body).trigger(type, params);
},
subscribe: function(type, data, callback){
$(document.body).bind(type, data, callback);
},
};
});
It works for what I want it for, but it has not been extensively tested.
As explunit points out in his answer, this will capture any events on document.body. I also discovered a scoping issue when accessing this inside of a passed callback function.
I wanna know if there is anyway to depend event binding with "if" in Backbone.
For example, if i have user profile model and i want to bind "Send Message" button event only if the attribute "acceptMsgs" sets true.
My current solution is to check it in the event firing, if there is better way, pls correct me.
I'm not sure if it's a better way to do it, but you can use a function that returns a hash for the event hash (and of course in the function you can check for some condition).
For example something along the lines of
myView = Backbone.Views.extend({
events: function () {
if (someCondition) {
return { "#someButton click" : "nameOfFunction"}
}
}
//the rest of your view
});
Alternatively you can forgo the event hash and bind your events in the initialize method, for example
initialize: function (options) {
if (someCondition) {
this.$el.on("click", "#someButton", nameOfFunction);
}
}