I have been using a beta version of 1.3 and now after moving to 1.3.1 I notice a problem which by checking all earlier versions I see it appears to have started in 1.3.0 rc1.
I have code like this:
<select ng-model="home.modal.topicId"
ng-change="ctrl.modalTopicChanged()"
ng-options="item.id as item.name for item in home.modal.option.topics.data"
ng-required="true">
<option style="display: none;" value="">Select Topic</option>
</select>
Prior to rc1 the ng-change was not being fired when the form was first displayed. Now it is being fired with a home.modal.topicId of undefined. This is a breaking change for me but it's not mentioned in the breaking change section and I wonder if it's a bug that has yet to be noticed.
Here is the stack trace produced:
TypeError: Cannot read property 'dataMap' of undefined
at AdminProblemController.modalTopicChanged (http://127.0.0.1:17315/Content/app/admin/controllers/ProblemController.js:109:114)
at $parseFunctionCall (http://127.0.0.1:17315/Scripts/angular.js:11387:18)
at Scope.$get.Scope.$eval (http://127.0.0.1:17315/Scripts/angular.js:13276:28)
at http://127.0.0.1:17315/Scripts/angular.js:19888:13
at http://127.0.0.1:17315/Scripts/angular.js:19499:9
at forEach (http://127.0.0.1:17315/Scripts/angular.js:331:20)
at $$writeModelToScope (http://127.0.0.1:17315/Scripts/angular.js:19497:5)
at writeToModelIfNeeded (http://127.0.0.1:17315/Scripts/angular.js:19490:14)
at http://127.0.0.1:17315/Scripts/angular.js:19484:9
at validationDone (http://127.0.0.1:17315/Scripts/angular.js:19420:9)
What I notice here is a new function: writeToModelIfNeeded
When I look at the change log differences I cannot find any mention of this function being introduced when I check all the changes and the line numbers.
I would like to get some advice on this. Firstly is it possible to find the change that caused the addition of the writeToModelIfNeeded then secondly is this the correct functionality for the select box. I thought the whole idea was that the ng-change would only fire if the model value was defined.
For reference here's the area of new code that seems to have been added with 1.3.0 rc.1
**
* #ngdoc method
* #name ngModel.NgModelController#$commitViewValue
*
* #description
* Commit a pending update to the `$modelValue`.
*
* Updates may be pending by a debounced event or because the input is waiting for a some future
* event defined in `ng-model-options`. this method is rarely needed as `NgModelController`
* usually handles calling this in response to input events.
*/
this.$commitViewValue = function() {
var viewValue = ctrl.$viewValue;
$timeout.cancel(pendingDebounce);
// If the view value has not changed then we should just exit, except in the case where there is
// a native validator on the element. In this case the validation state may have changed even though
// the viewValue has stayed empty.
if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
return;
}
ctrl.$$lastCommittedViewValue = viewValue;
// change to dirty
if (ctrl.$pristine) {
ctrl.$dirty = true;
ctrl.$pristine = false;
$animate.removeClass($element, PRISTINE_CLASS);
$animate.addClass($element, DIRTY_CLASS);
parentForm.$setDirty();
}
this.$$parseAndValidate();
};
this.$$parseAndValidate = function() {
var parserValid = true,
viewValue = ctrl.$$lastCommittedViewValue,
modelValue = viewValue;
for(var i = 0; i < ctrl.$parsers.length; i++) {
modelValue = ctrl.$parsers[i](modelValue);
if (isUndefined(modelValue)) {
parserValid = false;
break;
}
}
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
// ctrl.$modelValue has not been touched yet...
ctrl.$modelValue = ngModelGet();
}
var prevModelValue = ctrl.$modelValue;
var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
if (allowInvalid) {
ctrl.$modelValue = modelValue;
writeToModelIfNeeded();
}
ctrl.$$runValidators(parserValid, modelValue, viewValue, function() {
if (!allowInvalid) {
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
writeToModelIfNeeded();
}
});
function writeToModelIfNeeded() {
if (ctrl.$modelValue !== prevModelValue) {
ctrl.$$writeModelToScope();
}
}
};
this.$$writeModelToScope = function() {
ngModelSet(ctrl.$modelValue);
forEach(ctrl.$viewChangeListeners, function(listener) {
try {
listener();
} catch(e) {
$exceptionHandler(e);
}
});
};
I was able to reproduce your issue by doing this. Without seeing your controller though not sure if same thing:
this.modal = {
topicId:null,
option:{
topics:{
data:[{id:1,name:'item1'},{id:2,name:'item2'}]
}
}
};
What is happening here is that angular says null is an invalid value so by default sets it to undefined. You can fix this by setting it to 'undefined' or adding this to your html:
ng-model-options="{allowInvalid:true}"
also tested Josep plunker and changing that value to null also caused ngChange to fire
Related
I want to detect when a user has entered values into any form field by using the $dirty property and setting a flag accordingly. Not surprisingly, this works:
$scope.$watch('formDetails.$dirty', function() {
USR.userInputRecorded = true;
});
But I'd also like to detect when/if the user has emptied all fields and effectively restored the form to its original empty state. The snippet below does not work and I'm not sure why. Is there a way to watch for when the form changes back to "not dirty"?
$scope.$watch('formDetails.$pristine', function() {
USR.userInputRecorded = false;
});
Thanks.
Try this:
$scope.$watch('formDetails.$dirty', function(value) {
if (value === '') {
// field has been emptied;
your.form.$setPristine(true);
} else {
USR.userInputRecorded = true;
}
});
In my app, I am boradcasting a event for certain point, with checking some value. it works fine But the issue is, later on whenever i am trigger the broadcast, still my conditions works, that means my condition is working all times after the trigger happend.
here is my code :
scope.$watch('ctrl.data.deviceCity', function(newcity, oldcity) {
if (!newcity) {
scope.preloadMsg = false;
return;
}
scope.$on('cfpLoadingBar:started', function() {
$timeout(function() {
if (newcity && newcity.originalObject.stateId) { //the condition not works after the first time means alwasy appends the text
console.log('each time');
$('#loading-bar-spinner').find('.spinner-icon span')
.text('Finding install sites...');
}
}, 100);
});
});
you can deregister the watcher by storing its reference in a variable and then calling it:
var myWatch = scope.$watch('ctrl.data.deviceCity', function(){
if( someCondition === true ){
myWatch(); //deregister the watcher by calling its reference
}
});
if you want to switch logic, just set some variable somewhere that dictates the control flow of the method:
var myWatch = scope.$watch('ctrl.data.deviceCity', function(){
scope.calledOnce = false;
if(!scope.calledOnce){
//... run this the first time
scope.calledOnce = true;
}
else {
// run this the second time (and every other time if you do not deregister this watch or change the variable)
// if you don't need this $watch anymore afterwards, just deregister it like so:
myWatch();
}
})
I been trying to do checkbox Checkall and UnCheckall using subscribe and i'm partially successful doing that but i am unable to find a fix in couple of scenarios when i am dealing with subscribe .
Using subscribe :
I am here able to checkAll uncheckAll but when i uncheck a child checkbox i.e test1 or test2 i need my parent checkbox name also to be unchecked and in next turn if i check test1 the parent checkbox should be checked i.e keeping condition both child checkboxes are checked .
For fiddle : Click Here
ViewModel :
self.selectedAllBox.subscribe(function (newValue) {
if (newValue == true) {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(true);
});
} else {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(false);
});
}
});
The same scenario can be done perfectly in easy way using computed but due some performance issues i need to use subscribe which is best way it wont fire like computed onload .
Reference : Using computed same thing is done perfectly check this Fiddle
I tried to use change event in individual checkbox binding but its a dead end till now.
Any help is appreciated .
Your subscription only applies to edits on the selectedAllBox. To do what you want, you'll need subscriptions on every Person checkbox as well, to check for the right conditions and uncheck the selectedAllBox in the right situations there.
It strikes me as odd that this would be acceptable but using computed() is not. Maybe you should reconsider that part of your answer. I would much rather compute a "isAllSelected" value based on my viewModel state, then bind the selectedAllBox to that.
I solved a similar problem in my own application a couple of years ago using manual subscriptions. Although the computed observable method is concise and easy to understand, it suffers from poor performance when there's a large number of items. Hopefully the code below speaks for itself:
function unsetCount(array, propName) {
// When an item is added to the array, set up a manual subscription
function addItem(item) {
var previousValue = !!item[propName]();
item[propName]._unsetSubscription = item[propName].subscribe(function (latestValue) {
latestValue = !!latestValue;
if (latestValue !== previousValue) {
previousValue = latestValue;
unsetCount(unsetCount() + (latestValue ? -1 : 1));
}
});
return previousValue;
}
// When an item is removed from the array, dispose the subscription
function removeItem(item) {
item[propName]._unsetSubscription.dispose();
return !!item[propName]();
}
// Initialize
var tempUnsetCount = 0;
ko.utils.arrayForEach(array(), function (item) {
if (!addItem(item)) {
tempUnsetCount++;
}
});
var unsetCount = ko.observable(tempUnsetCount);
// Subscribe to array changes
array.subscribe(function (changes) {
var tempUnsetCount = unsetCount();
ko.utils.arrayForEach(changes, function (change) {
if (change.moved === undefined) {
if (change.status === 'added') {
if (!addItem(change.value))
tempUnsetCount++;
} else {
if (!removeItem(change.value))
tempUnsetCount--;
}
}
});
unsetCount(tempUnsetCount);
}, null, 'arrayChange');
return unsetCount;
}
You'll still use a computed observable in your viewmodel for the the select-all value, but now it'll only need to check the unselected count:
self.unselectedPeopleCount = unsetCount(self.People, 'Selected');
self.SelectAll = ko.pureComputed({
read: function() {
return self.People().length && self.unselectedPeopleCount() === 0;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:0});
Example: http://jsfiddle.net/mbest/dwnv81j0/
The computed approach is the right way to do this. You can improve some performance issues by using pureComputed and by using rateLimit. Both require more recent versions of Knockout than the 2.2.1 used in your example (3.2 and 3.1, respectively).
self.SelectAll = ko.pureComputed({
read: function() {
var item = ko.utils.arrayFirst(self.People(), function(item) {
return !item.Selected();
});
return item == null;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:1});
http://jsfiddle.net/mbest/AneL9/98/
Is there a way to force angular to re-render things bound to a property, even though the property has not changed value? e.g:
$scope.size = 1;
<div some-prop="size"></div>
$scope.$needsRender('size') // psuedocode
$scope.$apply(); // re-renders the <div>
Unfortunately I can't manage the property entirely in angular for performance reasons, which is why I need this "reset".
Angular DOM beeing manipulated outside of angular is ugly. Well to say the truth, it's more than that. People doing this probably deserves to die. slowly. Painfully.
But anyway yes, it is possible.
Short answer : You can do this be forcing the execution of a scope's watcher to fire.
module.factory("scopeUtils", function($parse) {
var scopeUtils = {
/**
* Apply watchers of given scope even if a digest progress is already in process on another level.
* This will only do a one-time cycle of watchers, without cascade digest.
*
* Please note that this is (almost) a hack, behaviour may be hazardous so please use with caution.
*
* #param {Scope} scope : scope to apply watchers from.
*/
applyWatchers : function(scope) {
scopeUtils.traverseScopeTree(scope, function(scope) {
var watchers = scope.$$watchers;
if(!watchers) {
return;
}
var watcher;
for(var i=0; i<watchers.length; i++) {
watcher = watchers[i];
var value = watcher.get(scope);
watcher.fn(value, value, scope);
}
});
},
traverseScopeTree : function(parentScope, traverseFn) {
var next,
current = parentScope,
target = parentScope;
do {
traverseFn(current);
if (!(next = (current.$$childHead ||
(current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while((current = next));
}
};
return scopeUtils;
});
Use it simply like that :
scopeUtils.applyWatchers(myScope);
<input type="checkbox" data-bind="checked: appealsFromThisCase, event: { change: onappealsFromThisCaseChange}" id="appealsforCaseCheckBox"/>
vm.onappealsFromThisCaseChange = function () {
if (vm.appealsFromThisCase())
{
vm.predicate(new breeze.Predicate("CaseId", "==", caseID));
return datacontext.getCaseAppeals().then(function () {
return true;
});
}
}
else
return vm.getAppeals();
Above is a checkbox which is bound to appealsFromThisCase observable which is initially false.
On change event, onappealsFromThisCaseChange is fired and I see that the observable appealsFromThisCase gives true when checkbox is unchecked and false when the checkbox is checked.
It looks like ko handles the change event in a strange way. Fiddle: http://jsfiddle.net/wnLyV/1/
js:
var VM = function(){
var self = this;
self.appealsFromThisCase = ko.observable(false);
self.appealsFromThisCase.subscribe(function(value){
console.log("from subscribe: " + value);
})
self.onappealsFromThisCaseClick = function(data){
console.log("from click: " + data.appealsFromThisCase());
return true;
}
self.onappealsFromThisCaseChange = function(data){
console.log("from change: " + data.appealsFromThisCase());
return true;
}
}
ko.applyBindings(new VM());
Also see these:
knockout.js and listen to check event on checkbox
Knockout checkbox change event sends old value
Can you replace the change event binding with a click binding, or with a subscription, like in the fiddle, or do you specifically need change?
EDIT:
So, based on the comments below, the final answer is:
Wrap onappealsFromThisCaseChange function into another function, which will be the actual handler and always return true. So just make another handler like this:
function(){
onappealsFromThisCaseChange();
return true;
}