In my application I $watch if the form is valid before doing some stuff. The problem is that ngForm won't compile models before I use it.
Exemple : http://plnkr.co/edit/Y7dL67Fn7SaSEkjiFf2q?p=preview
JS
$scope.results = [];
$scope.$watch(function() {
return $scope.testForm.$valid;
},
function( valid ) {
$scope.results.push( valid );
}
)
HTML
<ng-form name="testForm" ng-init="test = 1">
<input ng-model="test" required>
</ng-form>
<p ng-repeat="result in results track by $index" ng-class="{'false': !result, 'true': result}">{{ result }}</p>
Results :
false // Wrong
true
The form shouldn't be invalid at first because $scope.test is set to 1.
Any clue ?
According to the docs:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization.
It (almost always) makes sense to ignore this first call using a check:
$scope.$watch('testForm.$valid', function (newValue, oldValue) {
if (newValue === oldValue) { return; }
$scope.results.push(newValue);
});
See, also, this short demo.
Not sure if correctly understand, but it could be that the first time AngularJS checks,
$scope.results = [];
is empty, therefore result evaluating to false before you push anything in it.
If you start with an non-empty result, say:
$scope.results = [1];
First evaluation is truey.
I don't think $watch is the proper method. I think your issue has to do with how $watch works, and the digest cycle of Angular.
Related
I have an angular $scope variable that gets instantiated through a window event. The $scope variable is displayed correctly to the user, but updates are not being set through ng-model. What's odd is that ng-change has the correct value that I can use to manually set the scope variable.
I really don't understand why this is happening.
Event Handler:
document.addEventListener('updateSearchBar', handleUpdate);
function handleUpdate(eventData) {
$scope.benefitsFromDate = eventData.detail.fromDate;
$scope.$apply();
}
Front end:
<input
type="date"
name="fromDate"
ng-change="updateBenefitsFromDate(benefitsFromDate)"
ng-model="benefitsFromDate"
/>
<input
type="button"
value="Search"
class="btn btn-default"
ng-click="searchDocuments()"
ng-disabled="isSearchingDocuments"
/>
Angular snippet:
$scope.updateBenefitsFromDate = function(change) {
console.log($scope.benefitsFromDate); //still has old value
console.log(change); //has updated value when param is the $scope variable
//$scope.benefitsFromDate = change; //only way to update
};
$scope.searchDocuments = function() {
//ng-model never updates the variable when the value is changed and uses the instantiated value
console.log($scope.benefitsFromDate);
};
Why doesn't ng-model reflect the change, but passing $scope.benefitsFromDate in the ng-change function have the updated value?
I use the $timeout module when processing these kind of updates and avoid the call to $scope.$apply().
$timeout(function() {
$scope.benefitsFromDate = eventData.detail.fromDate;
});
I learned that angular is finicky about "dot rules".
All I had to do was prefix my data values with something else.
$scope.data = {};
$scope.data.benefitsFromDate = ...;
Or you can declare your controller to use as
ng-controller="appController as vm"
Say you have a template like
<a ng-show=function()>a link</a>
My question is: when is function run? How can I tell angular that it is time to re-run the function?
Well, ng-show takes a Boolean value, so your best option for using it is to call your function somewhere in the logic that sets ng-show.
$scope.show = false;
$scope.showSomething = function(myCondition){
if(myCondition){
myFunction();
$scope.show = true;
}
};
<div ng-show='show'></div>
Any expression in ng-show is evaluated at every digest cycle, so the function is run on every digest cycle as well. Thus any change to the value returned by the function, will reflect in the view. If your function makes any changes to the application state that triggers a new digest cycle, you may run into an infinite digest loop (which isn't great).
So be careful about using functions in directives like ng-show. If the function is pure though (it doesn't have any side-effects), you should be safe using it.
Here's a plunker that shows the function being called on every digest.
(Note: in the plunker example, clicking the button actually triggers two digest cycles, since the click event triggers one, and toggling the $scope variable triggers another.)
ng-show="booleanVar" takes a condition or a Boolean value . You can change the value of condition expression to hide and show values at run time .
But if you want some function to be called when ng-show is triggered then you can use $watch in your model .
<div ng-app="myApp" ng-controller="myCtrl" ng-init="isDisplayed = true">
<div ng-show="isDisplayed">something</div>
<button ng-click="isDisplayed = !isDisplayed">Toggle</button>
</div>
var myApp = angular.module('myApp', [])
.controller('myCtrl', function($scope, $log) {
$scope.$watch('isDisplayed', function(newValue, oldValue) {
if (newValue !== oldValue) {
$log.log('Changed!');
}
});
});
See details here
The following works:
<script>
angular.module('myApp', [])
.filter('myFilter', ['$rootScope', function($rootScope) {
return function(v) {
return $rootScope.capitalize ? (v && v.toUpperCase()) : v;
};
}])
.controller('myController', ['$rootScope', '$scope', function($rootScope, $scope) {
$scope.flipCapitalize = function() {
$rootScope.capitalize = !$rootScope.capitalize;
}
}]);
</script>
{{"Hello" | myFilter }}
<div ng-controller="myController">
<button ng-click="flipCapitalize()">flip</button>
</div>
The word "Hello" on the screen flips between mixed case and upper-case when you press the button.
But Angular doesn't "know" it is supposed to re-invoke the filter function. It just does so because the click restarts the digest look and it re-does everything.
My question: is that behavior guaranteed? Can I always assume that the filter will be reinvoked in the digest loop and I can use whatever data in any scope I can find? Or did I just get lucky?
My secondary question: if the filter function is reinvoked on every digest loop, is there some way I can defeat that behavior? Can I tell it, unless the arguments have changed, don't call this function again, you'll just get the same answer? Or do I have to memoize by hand?
According to the angular docs, if you want to guarantee your filter to work you need to mark it as "stateful" using the $stateful property of the filter.
It is strongly discouraged to write filters that are stateful, because the execution of those can't be optimized by Angular, which often leads to performance issues. Many stateful filters can be converted into stateless filters just by exposing the hidden state as a model and turning it into an argument for the filter.
If you however do need to write a stateful filter, you have to mark the filter as $stateful, which means that it will be executed one or more times during the each $digest cycle.
So you should mark your filter as stateful to guarantee that behavior:
.filter('myFilter', ['$rootScope', function($rootScope) {
var filter = function(v) {
return $rootScope.capitalize ? (v && v.toUpperCase()) : v;
};
filter.$stateful = true;
return filter;
}])
I'm looking to make a new angular directive which serves the purpose of comparing the input's ngmodel value to some other value, and checking them for equality.
If they are equal, I want this input to be valid. Otherwise, invalid.
<div ng-repeat="one in many">
<ng-form name="somethingToValidate">
<input type="text" ng-model="one.userTypedText"
required mustbeequalto="one.someOtherValue" />
</ng-form>
</div>
That's an example of the "mustbeequalto" directive in use when it's complete.
1) Please be aware that {{one.someOtherValue}} can change at ANY time, so this directive must be aware of the 2-way binding nature of this value.
2) I will be using this inside an ng-repeat, so it should be smart enough to only work within the scope of the particular ng-form containing it.
I think I need a validation directive, but if you think there is a better/moreelegant way, please advise. I've tried creating this directive and have failed miserably.
Write a function that returns the value of ng-model:
require: 'ngModel',
link: function(scope, element, attributes, ngModelController) {
var getModelValue = function() {
return ngModelController.$viewValue;
};
As shown above you can retrieve this via ngModelController.$viewValue. The ngModelController is available as the fourth argument in the link function by requiring it.
Write a function that returns the value of what is passed to the must-be-equal-to attribute:
var getMustBeEqualToValue = function() {
return scope.$eval(attributes.mustBeEqualTo);
};
You can use the $eval method to execute the expression on the current scope to get the correct value.
Write a function that sets the validity:
var setValidity = function (isValid) {
ngModelController.$setValidity('mustBeEqualTo', isValid);
};
Use $watch to execute setValidity everytime the value of ng-model changes:
scope.$watch(getModelValue, function(newValue, oldValue) {
if (newValue === oldValue) return;
setValidity(newValue === getMustBeEqualToValue());
});
Use $watch to execute setValidity everytime the value of what is passed to the must-be-equal-to attribute changes:
scope.$watch(getMustBeEqualToValue, function(newValue, oldValue) {
if (newValue === oldValue) return;
setValidity(getModelValue() === newValue);
});
Demo: http://plnkr.co/edit/w9t0uk6l0HL0QYi40Cth?p=preview
There is room for optimizations, but the example should hopefully be a good start.
I don't understand this, but I suspect I'm doing something wrong, or a non-angularjs way.
I have a checkbox list inside ng-repeat. It controller loads the list from a JSON. Pretty straightforward really. I'm then using a directive (car-select) on each of the resulting checkboxes. This directive calls a function inside the main $scope (selectBrand()). This cycles through the selected checkboxes, and if checked==true, add to $scope.brand. I've added a textbox so that $scope.brand fills it, and i've set it to required so that it fires the built in validation e.g:
HTML:
<div ng-repeat="v in viewModel">
<label class="checkbox">
<input type="checkbox" ng-model="v.c" ng-checked="v.c" />{{v.n}}
</label>
</div>
<input type="text" name="brands" ng-model="brands" car-select required/> <br>
JS:
$scope.selectBrand = function() {
var selectedBrands = [];
angular.forEach($scope.viewModel, function(v){
if (v.c)
selectedBrands.push(v.v);
})
if (selectedBrands.length > 0)
$scope.brands = selectedBrands;
else
$scope.brands = null;
}
DIRECTIVE:
app.directive('carSelect', function() {
return function(scope, element) {
element.bind('change', function() {
scope.selectBrand();
})
}
});
Here's the weird part which I don't understand. It took a while to figure out that this particular line was making this whole thing work. If I add the following in the page, everything works great. But if i remove it, the whole thing breaks. WHY?!
<div>{{selectBrand()}}</div>
It's like the whole thing doesn't bind unless the above is called in the HTML. It's called in the directive, and I've tried putting that call inside the clickButton() function, but eventually it breaks. Either way, the live update of the textbox seems to fail if the above is removed. I'd love to get a good explanation of how I'm doing something wrong and how I could fix it :)
PLUNKER:
http://plnkr.co/edit/4QISKcq7YYH678YLsTTF?p=preview
Ok, i create fork ;-)
update variable with only data checked
your model :
{"cars":
[
{"v":"m","n":"Mini","c":false},
{"v":"c","n":"Corvette","c":true},
{"v":"b","n":"BMW","c":true},
{"v":"l","n":"Lamborghini","c":true},
{"v":"f","n":"Ferrari","c":false}
]
}
you want only checked :
$scope.brands = $filter('filter')($scope.viewModel, {c: true});
when model change you want to update your variable so use watch in controller
$scope.$watch('viewModel', function(newval, oldval){
if (oldval != newval)
{
$scope.brands = $filter('filter')($scope.viewModel, {c: true});
}
},true
);
});
see http://plnkr.co/edit/PnABre?p=preview