I'm submitting a ...
Current behavior:
An error is thrown on mobile safari from Angular's internals:
null is not an object (evaluating 'this.$$scope.$root.$$phase')
Relevant lines of code from Angular source (error originates from the else if line):
var that = this;
if (debounceDelay > 0) {
this.$$pendingDebounce = this.$$timeout(function() {
that.$commitViewValue();
}, debounceDelay);
} else if (this.$$scope.$root.$$phase) {
this.$commitViewValue();
} else {
this.$$scope.$apply(function() {
that.$commitViewValue();
});
Expected / new behavior:
For the error not to throw.
Minimal reproduction of the problem with instructions:
Unfortunately I have none, this was caught in production on a mobile safari client. Could have been a one-off, as we've not encountered this error before, nor on other platforms/clients.
It originated from the this.$$debounceViewValueCommit(trigger); call in $setViewValue().
Could have been introduced in 1.6.6 or 1.6.5, as this error did not occur prior to that version.
AngularJS version: 1.6.6
Browser: Mobile safari iOS 10.3.1
Anything else:
TypeError: null is not an object (evaluating 'this.$$scope.$root.$$phase')
at $$debounceViewValueCommit(angular.js:13756:0)
at $setViewValue(angular.js:13737:0)
at call(angular.js:12304:0)
at handlerWrapper(angular.js:2078:0)
at apply(angular.js:2068:0)
at r(raven.js:294:0)
Any thoughts on what might have caused it and under what circumstances $root could be null?
This means that the scope was already destroyed by the time $$debounceViewValueCommit() was called.
This could be a result of many things, including Mobile Safari firing events out-of-order or app code manually calling $setViewValue() after the scope has been destroyed. It could also be a race condition happening due to deferring the $setViewValue() on certain events.
It is really hard to tell without a reproduction (or at least more info, such as what type of element was it, what was the trigger, what were the ngModelOptions etc).
The easy way out (assuming this is not a browser bug), would be checking if $$scope has already been destroyed inside $$debounceViewValueChange(), but we probably need to clearly define (and implement) how debouncing is handled when the input is destroyed. (E.g. Is the pending value committed immediately? Is it discarded?)
So the app code itself was not calling $setViewValue in this case. The only calls on ngModel that were made were:
this.ngModel.$setDirty();
this.ngModel.$validate();
However from the stack trace that was captured I can't confirm whether or not it happened after those calls or before.
There's not much to the component that triggered it, it's basically a dropdown from which a user selected a value, and that value got pushed into an array which was passed upstream to the parent component via an event.
Could a call to $setDirty() or $validate() on the model have triggered it?
Otherwise I think it may have been an out-of-order events thing.
Could a call to $setDirty() or $validate() on the model have triggered it?
Not afaict.
it's basically a dropdown from which a user selected a value, and that value got pushed into an array which was passed upstream to the parent component via an event
So, I assume it is a <select> element. Can you confirm? Are you using ngOptions?
value got pushed into an array which was passed upstream to the parent component via an event
Could this value update cause the element to be destroyed?
So, I assume it is a
<select>element. Can you confirm? Are you using ngOptions?
No, it's a custom dropdown with mainly simple elements, div, ul etc.
I am using ngOptions, in the following manner:
this.ngModel.$isEmpty = function() {
//Needed here to prevent $validate from setting the model to undefined
$ctrl.ngModel.$options = $ctrl.ngModel.$options.createChild({
allowInvalid: true,
});
//Return check now
return (!angular.isArray($ctrl.members) || $ctrl.members.length === 0);
};
Could this value update cause the element to be destroyed?
No, it wouldn't. The user would have to press a Next button first -- I assume they can't be that fast to press it before the selection was propagated. Unless Safari mucked the events up like you said earlier...
If it is a custom dropdown, when do you call the $setViewValue() method?
The user would have to press a Next button first -- I assume they can't be that fast to press it before the selection was propagated
I previously assumed that the error was about this.$$scope.$root (not being an object), which can only happen if this.$$scope was destroyed.
But it is possible that the error is about this.$$scope not being an object. This should never happen, though. But we do define $$scope as a getter (Object.defineProperty(this, '$$scope', {value: $scope})). I wonder if it is a bug with getters/setters in Mobile Safari (maybe caused by some (de-)optimization, thus only triggered is specific circumstances). Or maybe just a one-of, weird bug.
I don't call $setViewValue, the component passes on the newly selected value through an event to the parent component. That component updates the array with the values and that is passed back to the child component through one way data binding 馃槷
I think we should probably put it in the one-of weird bug basket ;) The component has been working fine for the past year or so, and this is the first time we've encountered this error.
I am using ngOptions, in the following manner:
...
I just realized you probably mean ngModelOptions (which is very different from ngOptions :smiley:)
(Also why are you creating a new $options instance whenever $isEmpty() is called? That seems redundant.)
I don't call $setViewValue
Interesting. Then I have no idea what is calling it (because ngModel doesn't do it on its own and we only do it for the built-in input/select/textarea directives). (ngOptions calls it too, but it only works on select elements and even then it checks that the scope has not been destroyed before calling it.)
That being said, (and although the stack trace you provided does not correspond to the "vanilla" angular.js v1.6.6 file), it seems that $setViewValue() is called inside an event handler. So, either someone (you? 3rd-party lib?) attached an event handler to your control and calls $setViewValue() or you are using a built-in directive (e.g. select) 馃槙
In any case, I am afraid there is not much we can do without more info (or a repro). I am going to close this, but feel free to continue the discussion below.
Also why are you creating a new $options instance whenever $isEmpty() is called? That seems redundant
Not sure -- that piece of code was refactored from pre 1.6 when the API for modifying the ngModelOptions changed. I even had a discussion about that (twice!) here on https://github.com/angular/angular.js/issues/12884, because $overrdeModelOptions didn't exist yet.
At the time of writing the original code, that was the only way I could make it to work. I remember it being a real pain. I must have just chucked it in place of the $$setOptions and carried on, happy it was working. Will probably have another look at it now with a clear head and refactor to $overrdeModelOptions while I'm at it.
the stack trace you provided does not correspond to the "vanilla" angular.js v1.6.6 file
It's being concatenated in a build process, I'm using the orginal sources, not the minified ones, to build a new minified vendor libraries file.
but it only works on select elements and even then it checks that the scope has not been destroyed before calling it
There's not a single <select> element in our entire codebase 馃拑
Perhaps it's a third party extension that somehow generates a selectbox for whatever reason on our page. I'll leave it on Sentry for a while and if we see more occurrences of it I'll check again! Thanks for looking into it. Don't you love puzzles like these 馃槅
Yep seems to work fine like:
//Override model options
this.ngModel.$overrideModelOptions({
allowInvalid: true,
});
//Empty check
this.ngModel.$isEmpty = function() {
return (!angular.isArray($ctrl.members) || $ctrl.members.length === 0);
};
Thanks for the heads up. Sometimes you get code blindness if you've been seeing something for too long 馃槅
Most helpful comment
Not sure -- that piece of code was refactored from pre 1.6 when the API for modifying the ngModelOptions changed. I even had a discussion about that (twice!) here on https://github.com/angular/angular.js/issues/12884, because
$overrdeModelOptionsdidn't exist yet.At the time of writing the original code, that was the only way I could make it to work. I remember it being a real pain. I must have just chucked it in place of the
$$setOptionsand carried on, happy it was working. Will probably have another look at it now with a clear head and refactor to$overrdeModelOptionswhile I'm at it.It's being concatenated in a build process, I'm using the orginal sources, not the minified ones, to build a new minified vendor libraries file.
There's not a single
<select>element in our entire codebase 馃拑Perhaps it's a third party extension that somehow generates a selectbox for whatever reason on our page. I'll leave it on Sentry for a while and if we see more occurrences of it I'll check again! Thanks for looking into it. Don't you love puzzles like these 馃槅