Stickit: How to trigger a change event after every model -> view change - backbone.js

I have many events bound to elements in my view, though when I use stickit js to change values in my view by altering the model it doesn't trigger an onChange event.
Is there a way that I can trigger an onchange event for the current model:element after the setting the value in the model without having to write a handler for every binding? This would be for all form elements, input/select/textarea.
I want to avoid the following for each form element on the page:
bindings: {
'#foo': {
observe: 'foo',
afterUpdate: 'forceChange'
},
'#bar': {
observe: 'bar',
afterUpdate: 'forceChange'
},
...
},
forceChange: function(el) { jQuery(el).change() }

One possible hack (with version 0.6.3 only) would be to define a global handler which matches all elements:
Backbone.Stickit.addHandler({
selector: '*',
afterUpdate: function($el) {
$el.trigger('change');
}
});
Since handlers are mixed in with other matching handlers and bindings configurations in the order that they are defined, you couldn't use afterUpdate in any of your bindings without overwriting this global, all-matching handler since the bindings configurations are the last to be mixed in. You can read more about it here.

Ahhh, that comment clarifies matters. So, in Javascript when you change an input's value "manually" (whether through jQuery or through someElement.value =) the browser won't, as you noticed, fire a change event. Change events (and most other events for that matter) are only fired in response to user actions, not to Javascript.
Luckily, just as you can "manually" change a value, you can also "manually" trigger an event. In jQuery the syntax for that is:
$(yourElement).trigger('change');
If you need to control things like e.target you can read up on the jQuery trigger documentation for the details, but that's the basic idea.
You can even chain the value-changing and event-triggering together if you want:
$(yourElement).val('newValue').trigger('change');

Related

Why does binding trigger a $watch although no data was changed?

I have an entity in the scope that is being edited by the user. Each time it gets modified, I want to trigger some custom validation. So I have:
// validate the position if anything has changed
$scope.$watch("entity", function() {
if ($scope.entity.Id) {
$scope.validate();
}
}, true /* watch "by value" (see: https://docs.angularjs.org/guide/scope#scope-life-cycle) */);
So far so good. Now, since this is a big entity with quite some fields, not all of the fields participate initially in data-binding. Only portions of the fields are visible to the user by using a tab control. When the user switches tabs, another portion of the entity is shown.
However, when the additional controls get bound to the corresponding fields of the entity, the $watch gets triggered even though the binding doesn't change any value on the entity.
Why is this the case and how could I prevent it?
Note:
I thought that the data-binding is possibly adding some internal $... fields to entity but these are hopefully disregarded (at least this is the case in angular.equals so they should probably be disregarded in the $watch too, I assume).
$watch with object equality flag true compares the properties of the old object copy with the current object. Therefore the listener fires when a property name is changed, added, or removed. You can try something like the following:
$scope.$watch("entity", function(newVal, oldVal) {
if(Object.keys(newVal).length !== Object.keys(oldVal).length)
return; //Detect added properties
if ($scope.entity.Id) {
$scope.validate();
}
}, true

Binding to property of model attribute in Backbone.js

Disclaimer: I'm new to Backbone.js (coming from AngularJS), so I may have an inaccurate mental model of how this is supposed to work.
I have an object, characterNodes, which I'm making an attribute on my model. characterNodes looks something like this:
var characterNodes = {
character_1: {
stories: [// list of Stories]
},
character_2: {
stories: [// list of Stories]
}
...
}
My Backbone Model looks something like this:
var StoryGraph = joint.dia.Graph.extend({
initialize: function() {
// Call parent constructor
StoryGraph.__super__.initialize.apply(this, []);
this.set('characterNodes', characterNodes);
this.on('change:characterNodes', function() {
alert('test');
});
}
});
Each Story has a property "isUnlocked" which is changed elsewhere in the application. I want to fire an event (ie. that is, the alert 'test' should pop up) whenever this property is changed. With the code as it is above, the event never seems to fire.
I can't get a clear understanding from the Backbone docs whether this is supposed to work - does on('change:characterNodes') fire whenever any property (or sub-property, or sub-sub-property, etc) of characterNodes changes? Or only when the pointer to the object changes, that is, when it's replaced with another object? Or am I doing something else wrong? Thanks!
Backbone doesn't do any magic, basically, the change event is fired only if you set the "characterNodes" to a new object. If you're changing a nested property of that object, Backbone doesn't know it happened. You have three options: a) Change the whole object (e.g. by creating a copy), b) fire the change event manually (m.trigger("change:characterNodes")) whenever you change a nested property, c) Do not use nested objects for this. Have "character1_Stories" as a top level property.
Options c) is preferable. Try to keep properties in your models flat. Option a) is also fine but it has the disadvantage of having to copy the object. Option b) is not recommended. This is because Backbone keeps track of the previous value of the model properties (m.previous("characterNodes")). If you change a nested property, the previous value will have the same reference to the same object as the new value, therefore, it won't reflect its previous state.
Try to call a function instead define the function, and pass the third argument to keep the context call.
Something like this:
this.on('change:characterNodes', this.AlertSomething, this);
Hope it helps.

Revalidate field with custom directive in AngularJS

I have this scenario where a field is invalid due to another selection on the form. When that selection changes I want to revalidate. I tried calling $setViewValue on the field when the selection changes, but that doesn't refire the validation. Any ideas?
I have a hack working, but I would prefer a clean solution.
I ran into the same issue and found a workaround/feature that appears to be undocumented. If you need to trigger ngModelController to revalidate, you can either do:
ngModelCtrl.$setViewValue(value, 'your event name', true);
or if you don't need to update your model value
ngModelCtrl.$commitViewValue(true);
The true in both cases above is a flag for revalidation. Without this flag, the issue I was running into was that if the model value does not change, then angular simply skips the validation. I am using this way to manually mark a custom control as $dirty
Source:
https://github.com/angular/angular.js/blob/master/src/ng/directive/input.js#L1843
Write a directive, say validation, and place it on your field in the definition of this directive in the:
link:function(scope,element,args){
element.bind('onfocus',function(){
// Your logic
})
// Similarly, bind other relevant events like key presses, etc.
}
Put a ng-change on your select and broadcast an event in it:
$rootScope.$broadcast("selectChangedEvent");
Then, in a directive you have placed on your field, simply put in the link function:
$rootScope.$on("selectChangedEvent", () => ngModelCtrl.$validate());
$validate runs each of the registered validators of your field.

Remove an event handler

I've added an event like so:
this.getLetters().element.on({
tap: me.lettersTap,
delegate: 'span.letter-hidden'
});
based on user interaction, I'm adding another class to those
elements, so it will be:
<span class="letter-hidden new_class">
I want to remove the event handler based on the "new_class",but got no clue how.
I tried like:
questionController.getLetters().element.un({
tap: questionController.lettersTap,
delegate: 'span.new_class',
scope: questionController
});
but it's not working, I am able to remove it if I replace the "new_class" with letter-hidden in the above code, so I suppose works to be removed if the same class is used, which is not mentioned in documentation. Am I doing something wrong, an alternative would be nice to have it...Thanks.
You used a delegate so it would be important to use exactly the same when you are trying to remove it. Otherwise ExtJS must expect it is a different element, which is a correct behavior.
This is how the registered listeners get checked:
(registered.fn === remove.fn &&
registered.scope === remove.scope &&
registered.delegate === remove.delegate &&
registered.order === remove.order)
So no, there is no alternative way. You have to unregister with the same properties as you registered it.
Note: Sencha don't use your properties to query within the DOM it just
lookup in the oberservalbe listeners collection for a listener that
was registered like the one you handed to unregister.

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.

Resources