I'm looking for a best-practice solution to avoid using $scope.$watch, given the following use-case:
1) directiveA has the following isloated scope:
{ sharedModel : '=' }
2) for its own usage, directiveA needs to change its inner state variable named modelA, based on sharedModel.
3) directiveB uses directiveA, and binds sharedModel to modelB (its internal model).
<directive-a
shared-model="vm.modelB" />
4) Whenever modelB/sharedModel changes, I would like modelA to be updated (remember, modelA data is only derived fromsharedModel).
In order to accomplish #4, I could add $scope.$watch on sharedModel. However, $watch is performance-expensive and not testable. Any best-practice reccomendations?
EDIT:
for live code example see jsfiddle.
notice $scope.$watch on line #10 which I wish to replace.
In this case it's possible to do in without additional pricy watch. You can make use of old-good ES5 property getters, it's going to be very efficient. So instead of $watch try something like this:
Object.defineProperty($scope, 'modelA', {
get() {
return $scope.sharedModel * 10;
}
});
Demo: http://jsfiddle.net/mggc611e/2/
Related
I'm an author of angular-input-modified directive.
This directive is used to track model's value and allows to check whether the value was modified and also provides reset() function to change value back to the initial state.
Right now, model's initial value is stored in the ngModelController.masterValue property and ngModelController.reset() function is provided. Please see the implementation.
I'm using the following statement: eval('$scope.' + modelPath + ' = modelCtrl.masterValue;'); in order to revert value back to it's initial state. modelPath here is actually a value of ng-model attribute. This was developed a way back and I don't like this approach, cause ng-model value can be a complex one and also nested scopes will break this functionality.
What is the best way to refactor this statement? How do I update model's value directly through the ngModel controller's interface?
The best solution I've found so far is to use the $parse service in order to parse the Angular's expression in the ng-model attribute and retrieve the setter function for it. Then we can change the model's value by calling this setter function with a new value.
Example:
function reset () {
var modelValueSetter = $parse(attrs.ngModel).assign;
modelValueSetter($scope, 'Some new value');
}
This works much more reliably than eval().
If you have a better idea please provide another answer or just comment this one. Thank you!
[previous answer]
I had trouble with this issue today, and I solved it by triggering and sort of hijacking the $parsers pipeline using a closure.
const hijack = {trigger: false; model: null};
modelCtrl.$parsers.push( val => {
if (hijack.trigger){
hijack.trigger = false;
return hijack.model;
}
else {
// .. do something else ...
})
Then for resetting the model you need to trigger the pipeline by changing the $viewValue with modelCtrl.$setViewValue('newViewValue').
const $setModelValue = function(model){
// trigger the hijack and pass along your new model
hijack.trigger = true;
hijack.model = model;
// assuming you have some logic in getViewValue to output a viewValue string
modelCtrl.$setViewValue( getViewValue(model) );
}
By using $setViewValue(), you will trigger the $parsers pipeline. The function I wrote in the first code block will then be executed with val = getViewValue(model), at which point it would try to parse it into something to use for your $modelValue according the logic in there. But at this point, the variable in the closure hijacks the parser and uses it to completely overwrite the current $modelValue.
At this point, val is not used in the $parser, but it will still be the actual value that is displayed in the DOM, so pick a nice one.
Let me know if this approach works for you.
[edit]
It seems that ngModel.$commitViewValue should trigger the $parsers pipeline as well, I tried quickly but couldn't get it to work.
Assuming a given form such as <form name="myForm">, it's easy enough to watch for validity, error, dirty state, etc. using a simple watch:
$scope.$watch('myForm.$valid', function() {
console.log('form is valid? ', $scope.myForm.$valid);
});
However, there doesn't appear to be an easy way to watch if any given input in this form has changed. Deep watching like so, does not work:
$scope.$watch('myForm', function() {
console.log('an input has changed'); //this will never fire
}, true);
$watchCollection only goes one level deep, which means I would have to create a new watch for every input. Not ideal.
What is an elegant way to watch a form for changes on any input without having to resort to multiple watches, or placing ng-change on each input?
Concerning the possible duplicate and your comment:
The directive solution in that question works, but it's not what I had in mind (i.e. not elegant, since it requires blur in order to work).
It works if you add true as third parameter for your $watch:
$scope.$watch('myFormdata', function() {
console.log('form model has been changed');
}, true);
Further information see the docs.
Working Fiddle (check console log)
Another more angular way would be to use angular's $pristine. This boolean property will be set to false once you manipulate the form model:
Fiddle
Based on my experience with my forms (new dev, but working with Angular for a while now), the elegant way to watch a form for changes is actually not to use any type of watch statement at all actually.
Use the built-in Angular boolean $pristine or $dirty and those values will change automatically on any input field or checkbox.
The catch is: it will not change the value if you add or splice from an array which had me stumped for a while.
The best fix for me was to manually do $scope.MyForm.$setDirty(); whenever I was adding or removing from my different arrays.
Worked like a charm!
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.
I'm new to AngularJS and have been assigned a maintenance task on an app we've inherited (originally developed for us by a third-party).
At the left of a header row in a table is a small button showing either a plus (+) or minus (-) symbol to indicate whether it will expand or collapse the section when clicked. It does this using ngClass as follows.
ng-class="{false:'icon-plus',true:'icon-minus'}[day.expanded]"
I have to remove the button when there is no data in the section and thus no ability to expand. There is already a class (.plus-placeholder) for this and I was wondering if the expressions that ngClass uses can be nested to allow something like this
ng-class="{false:'plus-placeholder',true:{false:'icon-plus',true:'icon-minus'}[day.expanded]}[day.hasTrips]"
which would allow me to add a hasTrips property to day to accomplish the task.
If this is not possible I think I will need to add a property something like expandoState that returns strings 'collapsed', 'expanded' and 'empty'. so I can code the ngClass like this
ng-class="{'collapsed':'icon-plus','expanded':'icon-minus','empty':'plus-placeholder'}[day.expandoState]"
And perhaps this is a cleaner way to do it in any case. Any thoughts/suggestions? Should it be relevant, the app is using AngularJS v1.0.2.
You certainly can do either of the two options you have mentioned. The second is far preferable to the first in terms of readable code.
The expandoState property you mention should probably be a property or a method placed on the scope. Your attribute would then read something like
ng-class="{'collapsed':'icon-plus','expanded':'icon-minus','empty':'plus-placeholder'}[expandoState()]"
To put this method on the scope you would need to find the relevant controller. This will probably be wherever day is assigned to the scope. Just add
$scope.expandoState = function() {
// Return current state name, based on $scope.day
};
Alternatively, you could write a method on the controller like
$scope.buttonClass = function() {
// Return button class name, based on $scope.day
};
that just returns the class to use. This will let you write the logic from your first option in a much more readable fashion. Then you can use
ng-class="buttonClass()"
Pretty new to backbone.js so forgive me of my ignorance. I'm wondering, is there a way to encapsulate functions within the View class specifically?
I ask because when setting default events...
events {
'click .something' : 'doSomething'
}
... I'd prefer to have doSomething be nested in an encapsulating object for optimal organization. For example:
ui: {
doSomething: function() {}
}
But then I can't seem to get the default events to work.
events {
'click .something' : 'ui.doSomething' // this doesn't work
}
Any help is greatly appreciated. Or, if you can tell me why I shouldn't be doing this then I'd appreciate that, as well. Thanks!
Looking through the source that binds the events (delegateEvents) which is called from the constructor, it is pretty clear that it works on variables with in the scope of the object.
http://documentcloud.github.com/backbone/docs/backbone.html#section-118
You could, however, override delegateEvents to be a bit smarter... You could parse the value for dots and chain your tokens. You could even check the type of the value and use an actual function in place of the string. That might give you better control the way you want.
More info on the delegateEvents function: http://documentcloud.github.com/backbone/#View-delegateEvents