From 51c516e7d4f2d10b0aaa4487bd0b52772022207a Mon Sep 17 00:00:00 2001 From: Austin O'Neil Date: Fri, 9 Sep 2016 15:34:29 -0600 Subject: [PATCH 0001/1014] docs(ngOptions): correct links remove redundant link to ngOptions and add link to ngRepeat PR (#15117) --- src/ng/directive/ngOptions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js index 8168b4110fdd..9b58f9b985fd 100644 --- a/src/ng/directive/ngOptions.js +++ b/src/ng/directive/ngOptions.js @@ -17,8 +17,8 @@ var ngOptionsMinErr = minErr('ngOptions'); * elements for the ``'s model is assigned via the `select` **`as`** part of the * comprehension expression * - reduced memory consumption by not creating a new scope for each repeated instance From 78e6a58368470cef3454b33acd8ee788f2eb88e2 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Sun, 5 Jun 2016 18:30:13 -0700 Subject: [PATCH 0002/1014] refactor($q): remove unnecessary checks/helpers/wrappers - Remove internal `makePromise()` helper. - Remove unnecessary wrapper functions. - Remove unnecessary check for promises resolving multiple times. (By following the Promises/A+ spec, we know this will never happen.) - Switch from function expressions to (named) function declarations. Closes #15065 --- src/ng/q.js | 60 +++++++++++++++++------------------------------------ 1 file changed, 19 insertions(+), 41 deletions(-) diff --git a/src/ng/q.js b/src/ng/q.js index 8664c992a43c..7b9de29b0d52 100644 --- a/src/ng/q.js +++ b/src/ng/q.js @@ -298,14 +298,14 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { * * @returns {Deferred} Returns a new instance of deferred. */ - var defer = function() { + function defer() { var d = new Deferred(); //Necessary to support unbound execution :/ d.resolve = simpleBind(d, d.resolve); d.reject = simpleBind(d, d.reject); d.notify = simpleBind(d, d.notify); return d; - }; + } function Promise() { this.$$state = { status: 0 }; @@ -331,9 +331,9 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { 'finally': function(callback, progressBack) { return this.then(function(value) { - return handleCallback(value, true, callback); + return handleCallback(value, resolve, callback); }, function(error) { - return handleCallback(error, false, callback); + return handleCallback(error, reject, callback); }, progressBack); } }); @@ -372,7 +372,7 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { } finally { --queueSize; if (errorOnUnhandledRejections && queueSize === 0) { - nextTick(processChecksFn()); + nextTick(processChecks); } } } @@ -389,25 +389,17 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { } } - function processChecksFn() { - return function() { processChecks(); }; - } - - function processQueueFn(state) { - return function() { processQueue(state); }; - } - function scheduleProcessQueue(state) { if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !state.pur) { if (queueSize === 0 && checkQueue.length === 0) { - nextTick(processChecksFn()); + nextTick(processChecks); } checkQueue.push(state); } if (state.processScheduled || !state.pending) return; state.processScheduled = true; ++queueSize; - nextTick(processQueueFn(state)); + nextTick(function() { processQueue(state); }); } function Deferred() { @@ -526,39 +518,27 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { * @param {*} reason Constant, message, exception or an object representing the rejection reason. * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ - var reject = function(reason) { + function reject(reason) { var result = new Deferred(); result.reject(reason); return result.promise; - }; - - var makePromise = function makePromise(value, resolved) { - var result = new Deferred(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - }; + } - var handleCallback = function handleCallback(value, isResolved, callback) { + function handleCallback(value, resolver, callback) { var callbackOutput = null; try { if (isFunction(callback)) callbackOutput = callback(); } catch (e) { - return makePromise(e, false); + return reject(e); } if (isPromiseLike(callbackOutput)) { return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); + return resolver(value); + }, reject); } else { - return makePromise(value, isResolved); + return resolver(value); } - }; + } /** * @ngdoc method @@ -578,11 +558,11 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { */ - var when = function(value, callback, errback, progressBack) { + function when(value, callback, errback, progressBack) { var result = new Deferred(); result.resolve(value); return result.promise.then(callback, errback, progressBack); - }; + } /** * @ngdoc method @@ -624,11 +604,9 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { forEach(promises, function(promise, key) { counter++; when(promise).then(function(value) { - if (results.hasOwnProperty(key)) return; results[key] = value; if (!(--counter)) deferred.resolve(results); }, function(reason) { - if (results.hasOwnProperty(key)) return; deferred.reject(reason); }); }); @@ -664,7 +642,7 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { return deferred.promise; } - var $Q = function Q(resolver) { + function $Q(resolver) { if (!isFunction(resolver)) { throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver); } @@ -682,7 +660,7 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { resolver(resolveFn, rejectFn); return deferred.promise; - }; + } // Let's make the instanceof operator work for promises, so that // `new $q(fn) instanceof $q` would evaluate to true. From d14c7f3c31deb098bf8f1c50ea6d00af758dbdcb Mon Sep 17 00:00:00 2001 From: Georgii Dolzhykov Date: Sat, 10 Sep 2016 02:15:00 +0300 Subject: [PATCH 0003/1014] docs($compile): remove obsolete sentence Fixes #15109 Closes #15119 --- src/ng/compile.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 4dcba455e5e5..8741dc4d074a 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -267,14 +267,14 @@ * and other directives used in the directive's template will also be excluded from execution. * * #### `scope` - * The scope property can be `true`, an object or a falsy value: + * The scope property can be `false`, `true`, or an object: * - * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope. + * * **`false` (default):** No scope will be created for the directive. The directive will use its + * parent's scope. * * * **`true`:** A new child scope that prototypically inherits from its parent will be created for * the directive's element. If multiple directives on the same element request a new scope, - * only one new scope is created. The new scope rule does not apply for the root of the template - * since the root of the template always gets a new scope. + * only one new scope is created. * * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's element. The * 'isolate' scope differs from normal scope in that it does not prototypically inherit from its parent From 912d5b9ad36efe3c4b8e0da4b20e2f3540472af8 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 12 Sep 2016 17:27:37 +0300 Subject: [PATCH 0004/1014] docs(ngView): remove obsolete known issue --- src/ngRoute/directive/ngView.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/ngRoute/directive/ngView.js b/src/ngRoute/directive/ngView.js index dba842f1eafa..ead89554f10f 100644 --- a/src/ngRoute/directive/ngView.js +++ b/src/ngRoute/directive/ngView.js @@ -26,13 +26,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); * * The enter and leave animation occur concurrently. * - * @knownIssue If `ngView` is contained in an asynchronously loaded template (e.g. in another - * directive's templateUrl or in a template loaded using `ngInclude`), then you need to - * make sure that `$route` is instantiated in time to capture the initial - * `$locationChangeStart` event and load the appropriate view. One way to achieve this - * is to have it as a dependency in a `.run` block: - * `myModule.run(['$route', function() {}]);` - * * @scope * @priority 400 * @param {string=} onload Expression to evaluate whenever the view updates. From 07849779ba365f371a8caa3b58e23f677cfdc5ad Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 12 Sep 2016 17:59:54 +0200 Subject: [PATCH 0005/1014] chore(benchmarks): fix order-by benchmark --- benchmarks/orderby-bp/app.js | 2 +- benchmarks/orderby-bp/jquery-noop.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 benchmarks/orderby-bp/jquery-noop.js diff --git a/benchmarks/orderby-bp/app.js b/benchmarks/orderby-bp/app.js index 849042b87980..32bcfaeb0e12 100644 --- a/benchmarks/orderby-bp/app.js +++ b/benchmarks/orderby-bp/app.js @@ -7,7 +7,7 @@ app.controller('DataController', function DataController($rootScope, $scope) { this.rows = []; var self = this; - $scope.benchmarkType = 'basic'; + $scope.benchmarkType = 'baseline'; $scope.rawProperty = function(key) { return function(item) { diff --git a/benchmarks/orderby-bp/jquery-noop.js b/benchmarks/orderby-bp/jquery-noop.js new file mode 100644 index 000000000000..8cac7fe4a149 --- /dev/null +++ b/benchmarks/orderby-bp/jquery-noop.js @@ -0,0 +1 @@ +// Override me with ?jquery=/bower_components/jquery/dist/jquery.js From 21e4db9e0783d1f22b043cdb053e3b8c155a2786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Freyre?= Date: Wed, 14 Sep 2016 13:56:45 +0200 Subject: [PATCH 0006/1014] docs(input[range]): fix erroneous examples PR (#15135) --- src/ng/directive/input.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 9b109793b90c..7e26ca16f6d1 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1116,7 +1116,7 @@ var inputType = {
Model as number:
Min:
- Max:
+ Max:
value = {{value}}
myForm.range.$valid = {{myForm.range.$valid}}
myForm.range.$error = {{myForm.range.$error}} @@ -1142,7 +1142,7 @@ var inputType = {
Model as number:
Min:
- Max:
+ Max:
value = {{value}}
myForm.range.$valid = {{myForm.range.$valid}}
myForm.range.$error = {{myForm.range.$error}} From 9e24e774a558143b3478536911a3a4c1714564ba Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Wed, 14 Sep 2016 13:21:09 -0700 Subject: [PATCH 0007/1014] perf(form, ngModel): change controllers to use prototype methods This makes the largetable-bp ng-model benchmarks 10-15% faster (down 90-100ms for me). The actual controller instantiation doesn't change too much but the overall numbers seem consistently faster, I assume all due to reducing memory usage / gc. Specifically on creation there is ~40% less memory GCed, on destruction there is about ~25% less. PR (#13286) BREAKING CHANGE: The use of prototype methods instead of new methods per instance removes the ability to pass NgModelController and FormController methods without context. For example `$scope.$watch('something', myNgModelCtrl.$render)` will no longer work because the `$render` method is passed without any context. This must now be replaced with ``` $scope.$watch('something', function() { myNgModelCtrl.$render(); }) ``` or possibly by using `Function.prototype.bind` or `angular.bind`. --- src/ng/directive/form.js | 322 +++++++++++++++-------- src/ng/directive/ngModel.js | 502 +++++++++++++++--------------------- 2 files changed, 425 insertions(+), 399 deletions(-) diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index dac2ebd05e5d..4790080854a8 100644 --- a/src/ng/directive/form.js +++ b/src/ng/directive/form.js @@ -1,6 +1,6 @@ 'use strict'; -/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true +/* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS */ var nullFormCtrl = { $addControl: noop, @@ -11,6 +11,7 @@ var nullFormCtrl = { $setPristine: noop, $setSubmitted: noop }, +PENDING_CLASS = 'ng-pending', SUBMITTED_CLASS = 'ng-submitted'; function nullFormRenameControl(control, name) { @@ -61,22 +62,28 @@ function nullFormRenameControl(control, name) { */ //asks for $scope to fool the BC controller module FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; -function FormController(element, attrs, $scope, $animate, $interpolate) { - var form = this, - controls = []; +function FormController($element, $attrs, $scope, $animate, $interpolate) { + this.$$controls = []; // init state - form.$error = {}; - form.$$success = {}; - form.$pending = undefined; - form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); - form.$dirty = false; - form.$pristine = true; - form.$valid = true; - form.$invalid = false; - form.$submitted = false; - form.$$parentForm = nullFormCtrl; + this.$error = {}; + this.$$success = {}; + this.$pending = undefined; + this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope); + this.$dirty = false; + this.$pristine = true; + this.$valid = true; + this.$invalid = false; + this.$submitted = false; + this.$$parentForm = nullFormCtrl; + + this.$$element = $element; + this.$$animate = $animate; + + setupValidity(this); +} +FormController.prototype = { /** * @ngdoc method * @name form.FormController#$rollbackViewValue @@ -88,11 +95,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * event defined in `ng-model-options`. This method is typically needed by the reset button of * a form that uses `ng-model-options` to pend updates. */ - form.$rollbackViewValue = function() { - forEach(controls, function(control) { + $rollbackViewValue: function() { + forEach(this.$$controls, function(control) { control.$rollbackViewValue(); }); - }; + }, /** * @ngdoc method @@ -105,11 +112,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` * usually handles calling this in response to input events. */ - form.$commitViewValue = function() { - forEach(controls, function(control) { + $commitViewValue: function() { + forEach(this.$$controls, function(control) { control.$commitViewValue(); }); - }; + }, /** * @ngdoc method @@ -132,29 +139,29 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * For example, if an input control is added that is already `$dirty` and has `$error` properties, * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form. */ - form.$addControl = function(control) { + $addControl: function(control) { // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored // and not added to the scope. Now we throw an error. assertNotHasOwnProperty(control.$name, 'input'); - controls.push(control); + this.$$controls.push(control); if (control.$name) { - form[control.$name] = control; + this[control.$name] = control; } - control.$$parentForm = form; - }; + control.$$parentForm = this; + }, // Private API: rename a form control - form.$$renameControl = function(control, newName) { + $$renameControl: function(control, newName) { var oldName = control.$name; - if (form[oldName] === control) { - delete form[oldName]; + if (this[oldName] === control) { + delete this[oldName]; } - form[newName] = control; + this[newName] = control; control.$name = newName; - }; + }, /** * @ngdoc method @@ -172,60 +179,26 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * different from case to case. For example, removing the only `$dirty` control from a form may or * may not mean that the form is still `$dirty`. */ - form.$removeControl = function(control) { - if (control.$name && form[control.$name] === control) { - delete form[control.$name]; + $removeControl: function(control) { + if (control.$name && this[control.$name] === control) { + delete this[control.$name]; } - forEach(form.$pending, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$error, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$$success, function(value, name) { - form.$setValidity(name, null, control); - }); - - arrayRemove(controls, control); + forEach(this.$pending, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$error, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$$success, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + + arrayRemove(this.$$controls, control); control.$$parentForm = nullFormCtrl; - }; - - - /** - * @ngdoc method - * @name form.FormController#$setValidity - * - * @description - * Sets the validity of a form control. - * - * This method will also propagate to parent forms. - */ - addSetValidityMethod({ - ctrl: this, - $element: element, - set: function(object, property, controller) { - var list = object[property]; - if (!list) { - object[property] = [controller]; - } else { - var index = list.indexOf(controller); - if (index === -1) { - list.push(controller); - } - } - }, - unset: function(object, property, controller) { - var list = object[property]; - if (!list) { - return; - } - arrayRemove(list, controller); - if (list.length === 0) { - delete object[property]; - } - }, - $animate: $animate - }); + }, /** * @ngdoc method @@ -237,13 +210,13 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * This method can be called to add the 'ng-dirty' class and set the form to a dirty * state (ng-dirty class). This method will also propagate to parent forms. */ - form.$setDirty = function() { - $animate.removeClass(element, PRISTINE_CLASS); - $animate.addClass(element, DIRTY_CLASS); - form.$dirty = true; - form.$pristine = false; - form.$$parentForm.$setDirty(); - }; + $setDirty: function() { + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$dirty = true; + this.$pristine = false; + this.$$parentForm.$setDirty(); + }, /** * @ngdoc method @@ -261,15 +234,15 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ - form.$setPristine = function() { - $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); - form.$dirty = false; - form.$pristine = true; - form.$submitted = false; - forEach(controls, function(control) { + $setPristine: function() { + this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); + this.$dirty = false; + this.$pristine = true; + this.$submitted = false; + forEach(this.$$controls, function(control) { control.$setPristine(); }); - }; + }, /** * @ngdoc method @@ -284,11 +257,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * Setting a form controls back to their untouched state is often useful when setting the form * back to its pristine state. */ - form.$setUntouched = function() { - forEach(controls, function(control) { + $setUntouched: function() { + forEach(this.$$controls, function(control) { control.$setUntouched(); }); - }; + }, /** * @ngdoc method @@ -297,12 +270,46 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * @description * Sets the form to its submitted state. */ - form.$setSubmitted = function() { - $animate.addClass(element, SUBMITTED_CLASS); - form.$submitted = true; - form.$$parentForm.$setSubmitted(); - }; -} + $setSubmitted: function() { + this.$$animate.addClass(this.$$element, SUBMITTED_CLASS); + this.$submitted = true; + this.$$parentForm.$setSubmitted(); + } +}; + +/** + * @ngdoc method + * @name form.FormController#$setValidity + * + * @description + * Sets the validity of a form control. + * + * This method will also propagate to parent forms. + */ +addSetValidityMethod({ + clazz: FormController, + set: function(object, property, controller) { + var list = object[property]; + if (!list) { + object[property] = [controller]; + } else { + var index = list.indexOf(controller); + if (index === -1) { + list.push(controller); + } + } + }, + unset: function(object, property, controller) { + var list = object[property]; + if (!list) { + return; + } + arrayRemove(list, controller); + if (list.length === 0) { + delete object[property]; + } + } +}); /** * @ngdoc directive @@ -549,3 +556,108 @@ var formDirectiveFactory = function(isNgForm) { var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); + + + +// helper methods +function setupValidity(instance) { + instance.$$classCache = {}; + instance.$$classCache[INVALID_CLASS] = !(instance.$$classCache[VALID_CLASS] = instance.$$element.hasClass(VALID_CLASS)); +} +function addSetValidityMethod(context) { + var clazz = context.clazz, + set = context.set, + unset = context.unset; + + clazz.prototype.$setValidity = function(validationErrorKey, state, controller) { + if (isUndefined(state)) { + createAndSet(this, '$pending', validationErrorKey, controller); + } else { + unsetAndCleanup(this, '$pending', validationErrorKey, controller); + } + if (!isBoolean(state)) { + unset(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } else { + if (state) { + unset(this.$error, validationErrorKey, controller); + set(this.$$success, validationErrorKey, controller); + } else { + set(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } + } + if (this.$pending) { + cachedToggleClass(this, PENDING_CLASS, true); + this.$valid = this.$invalid = undefined; + toggleValidationCss(this, '', null); + } else { + cachedToggleClass(this, PENDING_CLASS, false); + this.$valid = isObjectEmpty(this.$error); + this.$invalid = !this.$valid; + toggleValidationCss(this, '', this.$valid); + } + + // re-read the state as the set/unset methods could have + // combined state in this.$error[validationError] (used for forms), + // where setting/unsetting only increments/decrements the value, + // and does not replace it. + var combinedState; + if (this.$pending && this.$pending[validationErrorKey]) { + combinedState = undefined; + } else if (this.$error[validationErrorKey]) { + combinedState = false; + } else if (this.$$success[validationErrorKey]) { + combinedState = true; + } else { + combinedState = null; + } + + toggleValidationCss(this, validationErrorKey, combinedState); + this.$$parentForm.$setValidity(validationErrorKey, combinedState, this); + }; + + function createAndSet(ctrl, name, value, controller) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, controller); + } + + function unsetAndCleanup(ctrl, name, value, controller) { + if (ctrl[name]) { + unset(ctrl[name], value, controller); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(ctrl, className, switchValue) { + if (switchValue && !ctrl.$$classCache[className]) { + ctrl.$$animate.addClass(ctrl.$$element, className); + ctrl.$$classCache[className] = true; + } else if (!switchValue && ctrl.$$classCache[className]) { + ctrl.$$animate.removeClass(ctrl.$$element, className); + ctrl.$$classCache[className] = false; + } + } + + function toggleValidationCss(ctrl, validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false); + } +} + +function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + } + return true; +} diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index 006cab47aa6a..55310adceddd 100644 --- a/src/ng/directive/ngModel.js +++ b/src/ng/directive/ngModel.js @@ -6,7 +6,10 @@ DIRTY_CLASS: true, UNTOUCHED_CLASS: true, TOUCHED_CLASS: true, - $ModelOptionsProvider: true + PENDING_CLASS: true, + $ModelOptionsProvider: true, + addSetValidityMethod: true, + setupValidity: true */ var VALID_CLASS = 'ng-valid', @@ -15,7 +18,6 @@ var VALID_CLASS = 'ng-valid', DIRTY_CLASS = 'ng-dirty', UNTOUCHED_CLASS = 'ng-untouched', TOUCHED_CLASS = 'ng-touched', - PENDING_CLASS = 'ng-pending', EMPTY_CLASS = 'ng-empty', NOT_EMPTY_CLASS = 'ng-not-empty'; @@ -221,8 +223,8 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. * * */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', '$modelOptions', - /** @this */ function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate, $modelOptions) { +NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate', '$modelOptions']; +function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate, $modelOptions) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity. @@ -244,40 +246,53 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ this.$$parentForm = nullFormCtrl; this.$options = $modelOptions; - var parsedNgModel = $parse($attr.ngModel), - parsedNgModelAssign = parsedNgModel.assign, - ngModelGet = parsedNgModel, - ngModelSet = parsedNgModelAssign, - pendingDebounce = null, - parserValid, - ctrl = this; - - - this.$$initGetterSetters = function() { + this.$$parsedNgModel = $parse($attr.ngModel); + this.$$parsedNgModelAssign = this.$$parsedNgModel.assign; + this.$$ngModelGet = this.$$parsedNgModel; + this.$$ngModelSet = this.$$parsedNgModelAssign; + this.$$pendingDebounce = null; + this.$$parserValid = undefined; + + this.$$currentValidationRunId = 0; + + this.$$scope = $scope; + this.$$attr = $attr; + this.$$element = $element; + this.$$animate = $animate; + this.$$timeout = $timeout; + this.$$parse = $parse; + this.$$q = $q; + this.$$exceptionHandler = $exceptionHandler; + + setupValidity(this); + setupModelWatcher(this); +} - if (ctrl.$options.getOption('getterSetter')) { - var invokeModelGetter = $parse($attr.ngModel + '()'), - invokeModelSetter = $parse($attr.ngModel + '($$$p)'); +NgModelController.prototype = { + $$initGetterSetters: function() { + if (this.$options.getOption('getterSetter')) { + var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'), + invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)'); - ngModelGet = function($scope) { - var modelValue = parsedNgModel($scope); + this.$$ngModelGet = function($scope) { + var modelValue = this.$$parsedNgModel($scope); if (isFunction(modelValue)) { modelValue = invokeModelGetter($scope); } return modelValue; }; - ngModelSet = function($scope, newValue) { - if (isFunction(parsedNgModel($scope))) { + this.$$ngModelSet = function($scope, newValue) { + if (isFunction(this.$$parsedNgModel($scope))) { invokeModelSetter($scope, {$$$p: newValue}); } else { - parsedNgModelAssign($scope, newValue); + this.$$parsedNgModelAssign($scope, newValue); } }; - } else if (!parsedNgModel.assign) { + } else if (!this.$$parsedNgModel.assign) { throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}', - $attr.ngModel, startingTag($element)); + this.$$attr.ngModel, startingTag(this.$$element)); } - }; + }, /** @@ -300,7 +315,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be * invoked if you only change a property on the objects. */ - this.$render = noop; + $render: noop, /** * @ngdoc method @@ -320,57 +335,20 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @param {*} value The value of the input to check for emptiness. * @returns {boolean} True if `value` is "empty". */ - this.$isEmpty = function(value) { + $isEmpty: function(value) { // eslint-disable-next-line no-self-compare return isUndefined(value) || value === '' || value === null || value !== value; - }; + }, - this.$$updateEmptyClasses = function(value) { - if (ctrl.$isEmpty(value)) { - $animate.removeClass($element, NOT_EMPTY_CLASS); - $animate.addClass($element, EMPTY_CLASS); + $$updateEmptyClasses: function(value) { + if (this.$isEmpty(value)) { + this.$$animate.removeClass(this.$$element, NOT_EMPTY_CLASS); + this.$$animate.addClass(this.$$element, EMPTY_CLASS); } else { - $animate.removeClass($element, EMPTY_CLASS); - $animate.addClass($element, NOT_EMPTY_CLASS); + this.$$animate.removeClass(this.$$element, EMPTY_CLASS); + this.$$animate.addClass(this.$$element, NOT_EMPTY_CLASS); } - }; - - - var currentValidationRunId = 0; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$setValidity - * - * @description - * Change the validity state, and notify the form. - * - * This method can be called within $parsers/$formatters or a custom validation implementation. - * However, in most cases it should be sufficient to use the `ngModel.$validators` and - * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. - * - * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned - * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` - * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. - * The `validationErrorKey` should be in camelCase and will get converted into dash-case - * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` - * class and can be bound to as `{{someForm.someControl.$error.myError}}` . - * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), - * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. - * Skipped is used by Angular when validators do not run because of parse errors and - * when `$asyncValidators` do not run because any of the `$validators` failed. - */ - addSetValidityMethod({ - ctrl: this, - $element: $element, - set: function(object, property) { - object[property] = true; - }, - unset: function(object, property) { - delete object[property]; - }, - $animate: $animate - }); + }, /** * @ngdoc method @@ -383,12 +361,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * state (`ng-pristine` class). A model is considered to be pristine when the control * has not been changed from when first compiled. */ - this.$setPristine = function() { - ctrl.$dirty = false; - ctrl.$pristine = true; - $animate.removeClass($element, DIRTY_CLASS); - $animate.addClass($element, PRISTINE_CLASS); - }; + $setPristine: function() { + this.$dirty = false; + this.$pristine = true; + this.$$animate.removeClass(this.$$element, DIRTY_CLASS); + this.$$animate.addClass(this.$$element, PRISTINE_CLASS); + }, /** * @ngdoc method @@ -401,13 +379,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed * from when first compiled. */ - this.$setDirty = function() { - ctrl.$dirty = true; - ctrl.$pristine = false; - $animate.removeClass($element, PRISTINE_CLASS); - $animate.addClass($element, DIRTY_CLASS); - ctrl.$$parentForm.$setDirty(); - }; + $setDirty: function() { + this.$dirty = true; + this.$pristine = false; + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$$parentForm.$setDirty(); + }, /** * @ngdoc method @@ -421,11 +399,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * by default, however this function can be used to restore that state if the model has * already been touched by the user. */ - this.$setUntouched = function() { - ctrl.$touched = false; - ctrl.$untouched = true; - $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS); - }; + $setUntouched: function() { + this.$touched = false; + this.$untouched = true; + this.$$animate.setClass(this.$$element, UNTOUCHED_CLASS, TOUCHED_CLASS); + }, /** * @ngdoc method @@ -438,11 +416,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * touched state (`ng-touched` class). A model is considered to be touched when the user has * first focused the control element and then shifted focus away from the control (blur event). */ - this.$setTouched = function() { - ctrl.$touched = true; - ctrl.$untouched = false; - $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS); - }; + $setTouched: function() { + this.$touched = true; + this.$untouched = false; + this.$$animate.setClass(this.$$element, TOUCHED_CLASS, UNTOUCHED_CLASS); + }, /** * @ngdoc method @@ -532,11 +510,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * */ - this.$rollbackViewValue = function() { - $timeout.cancel(pendingDebounce); - ctrl.$viewValue = ctrl.$$lastCommittedViewValue; - ctrl.$render(); - }; + $rollbackViewValue: function() { + this.$$timeout.cancel(this.$$pendingDebounce); + this.$viewValue = this.$$lastCommittedViewValue; + this.$render(); + }, /** * @ngdoc method @@ -550,45 +528,46 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * If the validity changes to valid, it will set the model to the last available valid * `$modelValue`, i.e. either the last parsed value or the last value set from the scope. */ - this.$validate = function() { + $validate: function() { // ignore $validate before model is initialized - if (isNumberNaN(ctrl.$modelValue)) { + if (isNumberNaN(this.$modelValue)) { return; } - var viewValue = ctrl.$$lastCommittedViewValue; + var viewValue = this.$$lastCommittedViewValue; // Note: we use the $$rawModelValue as $modelValue might have been // set to undefined during a view -> model update that found validation // errors. We can't parse the view here, since that could change // the model although neither viewValue nor the model on the scope changed - var modelValue = ctrl.$$rawModelValue; + var modelValue = this.$$rawModelValue; - var prevValid = ctrl.$valid; - var prevModelValue = ctrl.$modelValue; + var prevValid = this.$valid; + var prevModelValue = this.$modelValue; - var allowInvalid = ctrl.$options.getOption('allowInvalid'); + var allowInvalid = this.$options.getOption('allowInvalid'); - ctrl.$$runValidators(modelValue, viewValue, function(allValid) { + var that = this; + this.$$runValidators(modelValue, viewValue, function(allValid) { // If there was no change in validity, don't update the model // This prevents changing an invalid modelValue to undefined if (!allowInvalid && prevValid !== allValid) { - // Note: Don't check ctrl.$valid here, as we could have + // Note: Don't check this.$valid here, as we could have // external validators (e.g. calculated on the server), // that just call $setValidity and need the model value // to calculate their validity. - ctrl.$modelValue = allValid ? modelValue : undefined; + that.$modelValue = allValid ? modelValue : undefined; - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); } } }); + }, - }; - - this.$$runValidators = function(modelValue, viewValue, doneCallback) { - currentValidationRunId++; - var localValidationRunId = currentValidationRunId; + $$runValidators: function(modelValue, viewValue, doneCallback) { + this.$$currentValidationRunId++; + var localValidationRunId = this.$$currentValidationRunId; + var that = this; // check parser error if (!processParseErrors()) { @@ -602,34 +581,34 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ processAsyncValidators(); function processParseErrors() { - var errorKey = ctrl.$$parserName || 'parse'; - if (isUndefined(parserValid)) { + var errorKey = that.$$parserName || 'parse'; + if (isUndefined(that.$$parserValid)) { setValidity(errorKey, null); } else { - if (!parserValid) { - forEach(ctrl.$validators, function(v, name) { + if (!that.$$parserValid) { + forEach(that.$validators, function(v, name) { setValidity(name, null); }); - forEach(ctrl.$asyncValidators, function(v, name) { + forEach(that.$asyncValidators, function(v, name) { setValidity(name, null); }); } // Set the parse error last, to prevent unsetting it, should a $validators key == parserName - setValidity(errorKey, parserValid); - return parserValid; + setValidity(errorKey, that.$$parserValid); + return that.$$parserValid; } return true; } function processSyncValidators() { var syncValidatorsValid = true; - forEach(ctrl.$validators, function(validator, name) { + forEach(that.$validators, function(validator, name) { var result = validator(modelValue, viewValue); syncValidatorsValid = syncValidatorsValid && result; setValidity(name, result); }); if (!syncValidatorsValid) { - forEach(ctrl.$asyncValidators, function(v, name) { + forEach(that.$asyncValidators, function(v, name) { setValidity(name, null); }); return false; @@ -640,7 +619,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ function processAsyncValidators() { var validatorPromises = []; var allValid = true; - forEach(ctrl.$asyncValidators, function(validator, name) { + forEach(that.$asyncValidators, function(validator, name) { var promise = validator(modelValue, viewValue); if (!isPromiseLike(promise)) { throw ngModelMinErr('nopromise', @@ -657,25 +636,25 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ if (!validatorPromises.length) { validationDone(true); } else { - $q.all(validatorPromises).then(function() { + that.$$q.all(validatorPromises).then(function() { validationDone(allValid); }, noop); } } function setValidity(name, isValid) { - if (localValidationRunId === currentValidationRunId) { - ctrl.$setValidity(name, isValid); + if (localValidationRunId === that.$$currentValidationRunId) { + that.$setValidity(name, isValid); } } function validationDone(allValid) { - if (localValidationRunId === currentValidationRunId) { + if (localValidationRunId === that.$$currentValidationRunId) { doneCallback(allValid); } } - }; + }, /** * @ngdoc method @@ -688,84 +667,87 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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; + $commitViewValue: function() { + var viewValue = this.$viewValue; - $timeout.cancel(pendingDebounce); + this.$$timeout.cancel(this.$$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)) { + if (this.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !this.$$hasNativeValidators)) { return; } - ctrl.$$updateEmptyClasses(viewValue); - ctrl.$$lastCommittedViewValue = viewValue; + this.$$updateEmptyClasses(viewValue); + this.$$lastCommittedViewValue = viewValue; // change to dirty - if (ctrl.$pristine) { + if (this.$pristine) { this.$setDirty(); } this.$$parseAndValidate(); - }; + }, - this.$$parseAndValidate = function() { - var viewValue = ctrl.$$lastCommittedViewValue; + $$parseAndValidate: function() { + var viewValue = this.$$lastCommittedViewValue; var modelValue = viewValue; - parserValid = isUndefined(modelValue) ? undefined : true; + var that = this; + + this.$$parserValid = isUndefined(modelValue) ? undefined : true; - if (parserValid) { - for (var i = 0; i < ctrl.$parsers.length; i++) { - modelValue = ctrl.$parsers[i](modelValue); + if (this.$$parserValid) { + for (var i = 0; i < this.$parsers.length; i++) { + modelValue = this.$parsers[i](modelValue); if (isUndefined(modelValue)) { - parserValid = false; + this.$$parserValid = false; break; } } } - if (isNumberNaN(ctrl.$modelValue)) { - // ctrl.$modelValue has not been touched yet... - ctrl.$modelValue = ngModelGet($scope); + if (isNumberNaN(this.$modelValue)) { + // this.$modelValue has not been touched yet... + this.$modelValue = this.$$ngModelGet(this.$$scope); } - var prevModelValue = ctrl.$modelValue; - var allowInvalid = ctrl.$options.getOption('allowInvalid'); - ctrl.$$rawModelValue = modelValue; + var prevModelValue = this.$modelValue; + var allowInvalid = this.$options.getOption('allowInvalid'); + this.$$rawModelValue = modelValue; if (allowInvalid) { - ctrl.$modelValue = modelValue; + this.$modelValue = modelValue; writeToModelIfNeeded(); } // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date. // This can happen if e.g. $setViewValue is called from inside a parser - ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) { + this.$$runValidators(modelValue, this.$$lastCommittedViewValue, function(allValid) { if (!allowInvalid) { - // Note: Don't check ctrl.$valid here, as we could have + // Note: Don't check this.$valid here, as we could have // external validators (e.g. calculated on the server), // that just call $setValidity and need the model value // to calculate their validity. - ctrl.$modelValue = allValid ? modelValue : undefined; + that.$modelValue = allValid ? modelValue : undefined; writeToModelIfNeeded(); } }); function writeToModelIfNeeded() { - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); } } - }; + }, - this.$$writeModelToScope = function() { - ngModelSet($scope, ctrl.$modelValue); - forEach(ctrl.$viewChangeListeners, function(listener) { + $$writeModelToScope: function() { + this.$$ngModelSet(this.$$scope, this.$modelValue); + forEach(this.$viewChangeListeners, function(listener) { try { listener(); } catch (e) { - $exceptionHandler(e); + // eslint-disable-next-line no-invalid-this + this.$$exceptionHandler(e); } - }); - }; + }, this); + }, /** * @ngdoc method @@ -817,16 +799,15 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @param {*} value value from the view. * @param {string} trigger Event that triggered the update. */ - this.$setViewValue = function(value, trigger) { - ctrl.$viewValue = value; - if (ctrl.$options.getOption('updateOnDefault')) { - ctrl.$$debounceViewValueCommit(trigger); + $setViewValue: function(value, trigger) { + this.$viewValue = value; + if (this.$options.getOption('updateOnDefault')) { + this.$$debounceViewValueCommit(trigger); } - }; + }, - this.$$debounceViewValueCommit = function(trigger) { - var options = ctrl.$options, - debounceDelay = options.getOption('debounce'); + $$debounceViewValueCommit: function(trigger) { + var debounceDelay = this.$options.getOption('debounce'); if (isNumber(debounceDelay[trigger])) { debounceDelay = debounceDelay[trigger]; @@ -834,20 +815,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ debounceDelay = debounceDelay['default']; } - $timeout.cancel(pendingDebounce); + this.$$timeout.cancel(this.$$pendingDebounce); + var that = this; if (debounceDelay) { - pendingDebounce = $timeout(function() { - ctrl.$commitViewValue(); + this.$$pendingDebounce = this.$$timeout(function() { + that.$commitViewValue(); }, debounceDelay); - } else if ($rootScope.$$phase) { - ctrl.$commitViewValue(); + } else if (this.$$scope.$root.$$phase) { + this.$commitViewValue(); } else { - $scope.$apply(function() { - ctrl.$commitViewValue(); + this.$$scope.$apply(function() { + that.$commitViewValue(); }); } - }; + } +}; +function setupModelWatcher(ctrl) { // model -> value // Note: we cannot use a normal scope.$watch as we want to detect the following: // 1. scope value is 'a' @@ -856,8 +840,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ // -> scope value did not change since the last digest as // ng-change executes in apply phase // 4. view should be changed back to 'a' - $scope.$watch(function ngModelWatch() { - var modelValue = ngModelGet($scope); + ctrl.$$scope.$watch(function ngModelWatch() { + var modelValue = ctrl.$$ngModelGet(ctrl.$$scope); // if scope model value and ngModel value are out of sync // TODO(perf): why not move this to the action fn? @@ -867,7 +851,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) ) { ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; - parserValid = undefined; + ctrl.$$parserValid = undefined; var formatters = ctrl.$formatters, idx = formatters.length; @@ -888,7 +872,39 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ return modelValue; }); -}]; +} + +/** + * @ngdoc method + * @name ngModel.NgModelController#$setValidity + * + * @description + * Change the validity state, and notify the form. + * + * This method can be called within $parsers/$formatters or a custom validation implementation. + * However, in most cases it should be sufficient to use the `ngModel.$validators` and + * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. + * + * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned + * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` + * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * class and can be bound to as `{{someForm.someControl.$error.myError}}` . + * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), + * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. + * Skipped is used by Angular when validators do not run because of parse errors and + * when `$asyncValidators` do not run because any of the `$validators` failed. + */ +addSetValidityMethod({ + clazz: NgModelController, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + } +}); /** @@ -1124,13 +1140,17 @@ var ngModelDirective = ['$rootScope', function($rootScope) { }); } + function setTouched() { + modelCtrl.$setTouched(); + } + element.on('blur', function() { if (modelCtrl.$touched) return; if ($rootScope.$$phase) { - scope.$evalAsync(modelCtrl.$setTouched); + scope.$evalAsync(setTouched); } else { - scope.$apply(modelCtrl.$setTouched); + scope.$apply(setTouched); } }); } @@ -1465,109 +1485,3 @@ function ModelOptions(options, parentOptions) { return new ModelOptions(options, _options); }; } - -// helper methods -function addSetValidityMethod(context) { - var ctrl = context.ctrl, - $element = context.$element, - classCache = {}, - set = context.set, - unset = context.unset, - $animate = context.$animate; - - classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS)); - - ctrl.$setValidity = setValidity; - - function setValidity(validationErrorKey, state, controller) { - if (isUndefined(state)) { - createAndSet('$pending', validationErrorKey, controller); - } else { - unsetAndCleanup('$pending', validationErrorKey, controller); - } - if (!isBoolean(state)) { - unset(ctrl.$error, validationErrorKey, controller); - unset(ctrl.$$success, validationErrorKey, controller); - } else { - if (state) { - unset(ctrl.$error, validationErrorKey, controller); - set(ctrl.$$success, validationErrorKey, controller); - } else { - set(ctrl.$error, validationErrorKey, controller); - unset(ctrl.$$success, validationErrorKey, controller); - } - } - if (ctrl.$pending) { - cachedToggleClass(PENDING_CLASS, true); - ctrl.$valid = ctrl.$invalid = undefined; - toggleValidationCss('', null); - } else { - cachedToggleClass(PENDING_CLASS, false); - ctrl.$valid = isObjectEmpty(ctrl.$error); - ctrl.$invalid = !ctrl.$valid; - toggleValidationCss('', ctrl.$valid); - } - - // re-read the state as the set/unset methods could have - // combined state in ctrl.$error[validationError] (used for forms), - // where setting/unsetting only increments/decrements the value, - // and does not replace it. - var combinedState; - if (ctrl.$pending && ctrl.$pending[validationErrorKey]) { - combinedState = undefined; - } else if (ctrl.$error[validationErrorKey]) { - combinedState = false; - } else if (ctrl.$$success[validationErrorKey]) { - combinedState = true; - } else { - combinedState = null; - } - - toggleValidationCss(validationErrorKey, combinedState); - ctrl.$$parentForm.$setValidity(validationErrorKey, combinedState, ctrl); - } - - function createAndSet(name, value, controller) { - if (!ctrl[name]) { - ctrl[name] = {}; - } - set(ctrl[name], value, controller); - } - - function unsetAndCleanup(name, value, controller) { - if (ctrl[name]) { - unset(ctrl[name], value, controller); - } - if (isObjectEmpty(ctrl[name])) { - ctrl[name] = undefined; - } - } - - function cachedToggleClass(className, switchValue) { - if (switchValue && !classCache[className]) { - $animate.addClass($element, className); - classCache[className] = true; - } else if (!switchValue && classCache[className]) { - $animate.removeClass($element, className); - classCache[className] = false; - } - } - - function toggleValidationCss(validationErrorKey, isValid) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - - cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true); - cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false); - } -} - -function isObjectEmpty(obj) { - if (obj) { - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) { - return false; - } - } - } - return true; -} From 76d3dafdeaf2f343d094b5a34ffb74adf64bb284 Mon Sep 17 00:00:00 2001 From: Prashant Singh Pawar Date: Thu, 15 Sep 2016 06:10:27 -0400 Subject: [PATCH 0008/1014] fix($compile): don't throw tplrt error when there is a whitespace around a top-level comment Added new conditional for NODE_TYPE_TEXT inside removeComments method of $compile Added corresponding unit tests. Closes #15108 PR (#15132) --- src/ng/compile.js | 5 +++-- test/ng/compileSpec.js | 48 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 8741dc4d074a..631ddc67cedd 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -3679,8 +3679,9 @@ function removeComments(jqNodes) { while (i--) { var node = jqNodes[i]; - if (node.nodeType === NODE_TYPE_COMMENT) { - splice.call(jqNodes, i, 1); + if (node.nodeType === NODE_TYPE_COMMENT || + (node.nodeType === NODE_TYPE_TEXT && node.nodeValue.trim() === '')) { + splice.call(jqNodes, i, 1); } } return jqNodes; diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index e4a8ddf4e960..70112d14a82f 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -1364,6 +1364,22 @@ describe('$compile', function() { }); }); + it('should ignore whitespace betwee comment and root node when replacing with a template', function() { + module(function() { + directive('replaceWithWhitespace', valueFn({ + replace: true, + template: '

Hello, world!

' + })); + }); + inject(function($compile, $rootScope) { + expect(function() { + element = $compile('
')($rootScope); + }).not.toThrow(); + expect(element.find('p').length).toBe(1); + expect(element.find('p').text()).toBe('Hello, world!'); + }); + }); + it('should keep prototype properties on directive', function() { module(function() { function DirectiveClass() { @@ -2092,6 +2108,18 @@ describe('$compile', function() { $compile('

'); $rootScope.$apply(); expect($exceptionHandler.errors).toEqual([]); + + // comments are ok + $templateCache.put('template.html', '
\n'); + $compile('

'); + $rootScope.$apply(); + expect($exceptionHandler.errors).toEqual([]); + + // white space around comments is ok + $templateCache.put('template.html', '
\n'); + $compile('

'); + $rootScope.$apply(); + expect($exceptionHandler.errors).toEqual([]); }); }); @@ -2303,6 +2331,26 @@ describe('$compile', function() { }); }); + it('should ignore whitespace between comment and root node when replacing with a templateUrl', function() { + module(function() { + directive('replaceWithWhitespace', valueFn({ + replace: true, + templateUrl: 'templateWithWhitespace.html' + })); + }); + inject(function($compile, $rootScope, $httpBackend) { + $httpBackend.whenGET('templateWithWhitespace.html'). + respond('

Hello, world!

'); + expect(function() { + element = $compile('
')($rootScope); + }).not.toThrow(); + $httpBackend.flush(); + expect(element.find('p').length).toBe(1); + expect(element.find('p').text()).toBe('Hello, world!'); + }); + }); + + it('should keep prototype properties on sync version of async directive', function() { module(function() { function DirectiveClass() { From 1547c751aa48efe7dbefef701c3df5983b04aa2e Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 6 Sep 2016 14:33:23 +0100 Subject: [PATCH 0009/1014] refactor($parse): remove Angular expression sandbox The angular expression parser (`$parse`) attempts to sandbox expressions to prevent unrestricted access to the global context. While the sandbox was not on the frontline of the security defense, developers kept relying upon it as a security feature even though it was always possible to access arbitrary JavaScript code if a malicious user could control the content of Angular templates in applications. This commit removes this sandbox, which has the following benefits: * it sends a clear message to developers that they should not rely on the sandbox to prevent XSS attacks; that they must prevent control of expression and templates instead. * it allows performance and size improvements in the core Angular 1 library. * it simplifies maintenance and provides opportunities to make the parser more capable. Please see the [Sandbox Removal Blog Post](http://angularjs.blogspot.com/2016/09/angular-16-expression-sandbox-removal.html) for more detail on what you should do to ensure that your application is secure. Closes #15094 --- docs/content/error/$parse/isecaf.ngdoc | 12 - docs/content/error/$parse/isecdom.ngdoc | 47 -- docs/content/error/$parse/isecff.ngdoc | 17 - docs/content/error/$parse/isecfld.ngdoc | 27 - docs/content/error/$parse/isecfn.ngdoc | 10 - docs/content/error/$parse/isecobj.ngdoc | 11 - docs/content/error/$parse/isecwindow.ngdoc | 45 -- docs/content/guide/expression.ngdoc | 6 +- docs/content/guide/security.ngdoc | 73 +- src/ng/parse.js | 264 +------ test/ng/directive/ngEventDirsSpec.js | 12 +- test/ng/parseSpec.js | 764 --------------------- 12 files changed, 66 insertions(+), 1222 deletions(-) delete mode 100644 docs/content/error/$parse/isecaf.ngdoc delete mode 100644 docs/content/error/$parse/isecdom.ngdoc delete mode 100644 docs/content/error/$parse/isecff.ngdoc delete mode 100644 docs/content/error/$parse/isecfld.ngdoc delete mode 100644 docs/content/error/$parse/isecfn.ngdoc delete mode 100644 docs/content/error/$parse/isecobj.ngdoc delete mode 100644 docs/content/error/$parse/isecwindow.ngdoc diff --git a/docs/content/error/$parse/isecaf.ngdoc b/docs/content/error/$parse/isecaf.ngdoc deleted file mode 100644 index 115e1b26d754..000000000000 --- a/docs/content/error/$parse/isecaf.ngdoc +++ /dev/null @@ -1,12 +0,0 @@ -@ngdoc error -@name $parse:isecaf -@fullName Assigning to Fields of Disallowed Context -@description - -Occurs when an expression attempts to assign a value on a field of any of the `Boolean`, `Number`, -`String`, `Array`, `Object`, or `Function` constructors or the corresponding prototypes. - -Angular bans the modification of these constructors or their prototypes from within expressions, -since it is a known way to modify the behaviour of existing functions/operations. - -To resolve this error, avoid assigning to fields of constructors or their prototypes in expressions. diff --git a/docs/content/error/$parse/isecdom.ngdoc b/docs/content/error/$parse/isecdom.ngdoc deleted file mode 100644 index 9f60e189ee92..000000000000 --- a/docs/content/error/$parse/isecdom.ngdoc +++ /dev/null @@ -1,47 +0,0 @@ -@ngdoc error -@name $parse:isecdom -@fullName Referencing a DOM node in Expression -@description - -Occurs when an expression attempts to access a DOM node. - -AngularJS restricts access to DOM nodes from within expressions since it's a known way to -execute arbitrary Javascript code. - -This check is only performed on object index and function calls in Angular expressions. These are -places that are harder for the developer to guard. Dotted member access (such as a.b.c) does not -perform this check - it's up to the developer to not expose such sensitive and powerful objects -directly on the scope chain. - -To resolve this error, avoid access to DOM nodes. - - -# Event Handlers and Return Values - -The `$parse:isecdom` error also occurs when an event handler invokes a function that returns a DOM -node. - -```html - -``` - -```js - $scope.iWillReturnDOM = function() { - return someDomNode; - } -``` - -To fix this issue, avoid returning DOM nodes from event handlers. - -*Note: This error often means that you are accessing DOM from your controllers, which is usually -a sign of poor coding style that violates separation of concerns.* - - -# Implicit Returns in CoffeeScript - -This error can occur more frequently when using CoffeeScript, which has a feature called implicit -returns. This language feature returns the last dereferenced object in the function when the -function has no explicit return statement. - -The solution in this scenario is to add an explicit return statement. For example `return false` to -the function. diff --git a/docs/content/error/$parse/isecff.ngdoc b/docs/content/error/$parse/isecff.ngdoc deleted file mode 100644 index a1e72775d254..000000000000 --- a/docs/content/error/$parse/isecff.ngdoc +++ /dev/null @@ -1,17 +0,0 @@ -@ngdoc error -@name $parse:isecff -@fullName Referencing 'call', 'apply' and 'bind' Disallowed -@description - -Occurs when an expression attempts to invoke Function's 'call', 'apply' or 'bind'. - -Angular bans the invocation of 'call', 'apply' and 'bind' from within expressions -since access is a known way to modify the behaviour of existing functions. - -To resolve this error, avoid using these methods in expressions. - -Example expression that would result in this error: - -``` -
{{user.sendInfo.call({}, true)}}
-``` diff --git a/docs/content/error/$parse/isecfld.ngdoc b/docs/content/error/$parse/isecfld.ngdoc deleted file mode 100644 index a19c5fa51e97..000000000000 --- a/docs/content/error/$parse/isecfld.ngdoc +++ /dev/null @@ -1,27 +0,0 @@ -@ngdoc error -@name $parse:isecfld -@fullName Referencing Disallowed Field in Expression -@description - -Occurs when an expression attempts to access one of the following fields: - -* __proto__ -* __defineGetter__ -* __defineSetter__ -* __lookupGetter__ -* __lookupSetter__ - -AngularJS bans access to these fields from within expressions since -access is a known way to mess with native objects or -to execute arbitrary Javascript code. - -To resolve this error, avoid using these fields in expressions. As a last resort, -alias their value and access them through the alias instead. - -Example expressions that would result in this error: - -``` -
{{user.__proto__.hasOwnProperty = $emit}}
- -
{{user.__defineGetter__('name', noop)}}
-``` diff --git a/docs/content/error/$parse/isecfn.ngdoc b/docs/content/error/$parse/isecfn.ngdoc deleted file mode 100644 index 417551cb3606..000000000000 --- a/docs/content/error/$parse/isecfn.ngdoc +++ /dev/null @@ -1,10 +0,0 @@ -@ngdoc error -@name $parse:isecfn -@fullName Referencing Function Disallowed -@description - -Occurs when an expression attempts to access the 'Function' object (constructor for all functions in JavaScript). - -Angular bans access to Function from within expressions since constructor access is a known way to execute arbitrary Javascript code. - -To resolve this error, avoid Function access. diff --git a/docs/content/error/$parse/isecobj.ngdoc b/docs/content/error/$parse/isecobj.ngdoc deleted file mode 100644 index 8da6e27a3505..000000000000 --- a/docs/content/error/$parse/isecobj.ngdoc +++ /dev/null @@ -1,11 +0,0 @@ -@ngdoc error -@name $parse:isecobj -@fullName Referencing Object Disallowed -@description - -Occurs when an expression attempts to access the 'Object' object (Root object in JavaScript). - -Angular bans access to Object from within expressions since access is a known way to modify -the behaviour of existing objects. - -To resolve this error, avoid Object access. diff --git a/docs/content/error/$parse/isecwindow.ngdoc b/docs/content/error/$parse/isecwindow.ngdoc deleted file mode 100644 index e7f4ceeaddd5..000000000000 --- a/docs/content/error/$parse/isecwindow.ngdoc +++ /dev/null @@ -1,45 +0,0 @@ -@ngdoc error -@name $parse:isecwindow -@fullName Referencing Window object in Expression -@description - -Occurs when an expression attempts to access a Window object. - -AngularJS restricts access to the Window object from within expressions since it's a known way to -execute arbitrary Javascript code. - -This check is only performed on object index and function calls in Angular expressions. These are -places that are harder for the developer to guard. Dotted member access (such as a.b.c) does not -perform this check - it's up to the developer to not expose such sensitive and powerful objects -directly on the scope chain. - -To resolve this error, avoid Window access. - -### Common CoffeeScript Issue - -Be aware that if you are using CoffeeScript, it automatically returns the value of the last statement in a -function. So for instance - -```coffeescript - scope.foo = -> - window.open 'https://example.com' -``` - -compiles to something like - -```js - scope.foo = function() { - return window.open('https://example.com'); - }; -``` - -You can see that this function will return the result of calling `window.open`, which is a `Window` -object. - -You can avoid this by explicitly returning something else from the function: - -```coffeescript - scope.foo = -> - window.open 'https://example.com' - return true; -``` diff --git a/docs/content/guide/expression.ngdoc b/docs/content/guide/expression.ngdoc index 824facd9dfb3..24326b6abae1 100644 --- a/docs/content/guide/expression.ngdoc +++ b/docs/content/guide/expression.ngdoc @@ -113,11 +113,11 @@ You can try evaluating different expressions here: Angular does not use JavaScript's `eval()` to evaluate expressions. Instead Angular's {@link ng.$parse $parse} service processes these expressions. -Angular expressions do not have access to global variables like `window`, `document` or `location`. +Angular expressions do not have direct access to global variables like `window`, `document` or `location`. This restriction is intentional. It prevents accidental access to the global state – a common source of subtle bugs. -Instead use services like `$window` and `$location` in functions called from expressions. Such services -provide mockable access to globals. +Instead use services like `$window` and `$location` in functions on controllers, which are then called from expressions. +Such services provide mockable access to globals. It is possible to access the context object using the identifier `this` and the locals object using the identifier `$locals`. diff --git a/docs/content/guide/security.ngdoc b/docs/content/guide/security.ngdoc index 175c49edf9c0..9fcf0e7c6078 100644 --- a/docs/content/guide/security.ngdoc +++ b/docs/content/guide/security.ngdoc @@ -30,42 +30,55 @@ so keeping to AngularJS standards is not just a functionality issue, it's also c facilitate rapid security updates. -## Expression Sandboxing - -AngularJS's expressions are sandboxed not for security reasons, but instead to maintain a proper -separation of application responsibilities. For example, access to `window` is disallowed -because it makes it easy to introduce brittle global state into your application. - -However, this sandbox is not intended to stop attackers who can edit the template before it's -processed by Angular. It may be possible to run arbitrary JavaScript inside double-curly bindings -if an attacker can modify them. - -But if an attacker can change arbitrary HTML templates, there's nothing stopping them from doing: - -```html - -``` - -**It's better to design your application in such a way that users cannot change client-side templates.** - -For instance: +## Angular Templates and Expressions + +**If an attacker has access to control Angular templates or expressions, they can exploit an Angular application +via an XSS attack, regardless of the version.** + +There are a number of ways that templates and expressions can be controlled: + +* **Generating Angular templates on the server containing user-provided content**. This is the most common pitfall + where you are generating HTML via some server-side engine such as PHP, Java or ASP.NET. +* **Passing an expression generated from user-provided content in calls to the following methods on a {@link scope scope}**: + * `$watch(userContent, ...)` + * `$watchGroup(userContent, ...)` + * `$watchCollection(userContent, ...)` + * `$eval(userContent)` + * `$evalAsync(userContent)` + * `$apply(userContent)` + * `$applyAsync(userContent)` +* **Passing an expression generated from user-provided content in calls to services that parse expressions**: + * `$compile(userContent)` + * `$parse(userContent)` + * `$interpolate(userContent)` +* **Passing an expression generated from user provided content as a predicate to `orderBy` pipe**: + `{{ value | orderBy : userContent }}` + +### Sandbox removal +Each version of Angular 1 up to, but not including 1.6, contained an expression sandbox, which reduced the surface area of +the vulnerability but never removed it. **In Angular 1.6 we removed this sandbox as developers kept relying upon it as a security +feature even though it was always possible to access arbitrary JavaScript code if one could control the Angular templates +or expressions of applications.** + +Control of the Angular templates makes applications vulnerable even if there was a completely secure sandbox: +* https://ryhanson.com/stealing-session-tokens-on-plunker-with-an-angular-expression-injection/ in this blog post the author shows + a (now closed) vulnerability in the Plunker application due to server-side rendering inside an Angular template. +* https://ryhanson.com/angular-expression-injection-walkthrough/ in this blog post the author describes an attack, which does not + rely upon an expression sandbox bypass, that can be made because the sample application is rendering a template on the server that + contains user entered content. + +**It's best to design your application in such a way that users cannot change client-side templates.** * Do not mix client and server templates * Do not use user input to generate templates dynamically -* Do not run user input through `$scope.$eval` +* Do not run user input through `$scope.$eval` (or any of the other expression parsing functions listed above) * Consider using {@link ng.directive:ngCsp CSP} (but don't rely only on CSP) +**You can use suitably sanitized server-side templating to dynamically generate CSS, URLs, etc, but not for generating templates that are +bootstrapped/compiled by Angular.** -### Mixing client-side and server-side templates - -In general, we recommend against this because it can create unintended XSS vectors. - -However, it's ok to mix server-side templating in the bootstrap template (`index.html`) as long -as user input cannot be used on the server to output html that would then be processed by Angular -in a way that would allow for arbitrary code execution. - -**For instance, you can use server-side templating to dynamically generate CSS, URLs, etc, but not -for generating templates that are bootstrapped/compiled by Angular.** +**If you must continue to allow user-provided content in an Angular template then the safest option is to ensure that it is only +present in the part of the template that is made inert via the {@link ngNonBindable} directive.** ## HTTP Requests diff --git a/src/ng/parse.js b/src/ng/parse.js index a008dd1e4fea..ed0c4ca43afc 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -13,60 +13,23 @@ var $parseMinErr = minErr('$parse'); -var ARRAY_CTOR = [].constructor; -var BOOLEAN_CTOR = (false).constructor; -var FUNCTION_CTOR = Function.constructor; -var NUMBER_CTOR = (0).constructor; -var OBJECT_CTOR = {}.constructor; -var STRING_CTOR = ''.constructor; -var ARRAY_CTOR_PROTO = ARRAY_CTOR.prototype; -var BOOLEAN_CTOR_PROTO = BOOLEAN_CTOR.prototype; -var FUNCTION_CTOR_PROTO = FUNCTION_CTOR.prototype; -var NUMBER_CTOR_PROTO = NUMBER_CTOR.prototype; -var OBJECT_CTOR_PROTO = OBJECT_CTOR.prototype; -var STRING_CTOR_PROTO = STRING_CTOR.prototype; - -var CALL = FUNCTION_CTOR_PROTO.call; -var APPLY = FUNCTION_CTOR_PROTO.apply; -var BIND = FUNCTION_CTOR_PROTO.bind; - -var objectValueOf = OBJECT_CTOR_PROTO.valueOf; +var objectValueOf = {}.constructor.prototype.valueOf; // Sandboxing Angular Expressions // ------------------------------ -// Angular expressions are generally considered safe because these expressions only have direct -// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by -// obtaining a reference to native JS functions such as the Function constructor. +// Angular expressions are no longer sandboxed. So it is now even easier to access arbitary JS code by +// various means such as obtaining a reference to native JS functions like the Function constructor. // // As an example, consider the following Angular expression: // // {}.toString.constructor('alert("evil JS code")') // -// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits -// against the expression language, but not to prevent exploits that were enabled by exposing -// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good -// practice and therefore we are not even trying to protect against interaction with an object -// explicitly exposed in this way. -// -// In general, it is not possible to access a Window object from an angular expression unless a -// window or some DOM object that has a reference to window is published onto a Scope. -// Similarly we prevent invocations of function known to be dangerous, as well as assignments to -// native objects. +// It is important to realise that if you create an expression from a string that contains user provided +// content then it is possible that your application contains a security vulnerability to an XSS style attack. // // See https://docs.angularjs.org/guide/security -function ensureSafeMemberName(name, fullExpression) { - if (name === '__defineGetter__' || name === '__defineSetter__' - || name === '__lookupGetter__' || name === '__lookupSetter__' - || name === '__proto__') { - throw $parseMinErr('isecfld', - 'Attempting to access a disallowed field in Angular expressions! ' - + 'Expression: {0}', fullExpression); - } - return name; -} - function getStringValue(name) { // Property names must be strings. This means that non-string objects cannot be used // as keys in an object. Any non-string object, including a number, is typecasted @@ -85,67 +48,6 @@ function getStringValue(name) { return name + ''; } -function ensureSafeObject(obj, fullExpression) { - // nifty check if obj is Function that is fast and works across iframes and other contexts - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isWindow(obj) - obj.window === obj) { - throw $parseMinErr('isecwindow', - 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isElement(obj) - obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { - throw $parseMinErr('isecdom', - 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// block Object so that we can't get hold of dangerous Object.* methods - obj === Object) { - throw $parseMinErr('isecobj', - 'Referencing Object in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } - return obj; -} - -function ensureSafeFunction(obj, fullExpression) { - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (obj === CALL || obj === APPLY || obj === BIND) { - throw $parseMinErr('isecff', - 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } -} - -function ensureSafeAssignContext(obj, fullExpression) { - if (obj) { - if (obj === ARRAY_CTOR || - obj === BOOLEAN_CTOR || - obj === FUNCTION_CTOR || - obj === NUMBER_CTOR || - obj === OBJECT_CTOR || - obj === STRING_CTOR || - obj === ARRAY_CTOR_PROTO || - obj === BOOLEAN_CTOR_PROTO || - obj === FUNCTION_CTOR_PROTO || - obj === NUMBER_CTOR_PROTO || - obj === OBJECT_CTOR_PROTO || - obj === STRING_CTOR_PROTO) { - throw $parseMinErr('isecaf', - 'Assigning to a constructor or its prototype is disallowed! Expression: {0}', - fullExpression); - } - } -} var OPERATORS = createMap(); forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); @@ -862,13 +764,12 @@ function ASTCompiler(astBuilder, $filter) { } ASTCompiler.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(expression) { var self = this; var ast = this.astBuilder.ast(expression); this.state = { nextId: 0, filters: {}, - expensiveChecks: expensiveChecks, fn: {vars: [], body: [], own: {}}, assign: {vars: [], body: [], own: {}}, inputs: [] @@ -911,21 +812,13 @@ ASTCompiler.prototype = { // eslint-disable-next-line no-new-func var fn = (new Function('$filter', - 'ensureSafeMemberName', - 'ensureSafeObject', - 'ensureSafeFunction', 'getStringValue', - 'ensureSafeAssignContext', 'ifDefined', 'plus', 'text', fnString))( this.$filter, - ensureSafeMemberName, - ensureSafeObject, - ensureSafeFunction, getStringValue, - ensureSafeAssignContext, ifDefined, plusFn, expression); @@ -1042,7 +935,6 @@ ASTCompiler.prototype = { nameId.computed = false; nameId.name = ast.name; } - ensureSafeMemberName(ast.name); self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), function() { self.if_(self.stage === 'inputs' || 's', function() { @@ -1055,9 +947,6 @@ ASTCompiler.prototype = { }); }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name)) ); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { - self.addEnsureSafeObject(intoId); - } recursionFn(intoId); break; case AST.MemberExpression: @@ -1065,32 +954,24 @@ ASTCompiler.prototype = { intoId = intoId || this.nextId(); self.recurse(ast.object, left, undefined, function() { self.if_(self.notNull(left), function() { - if (create && create !== 1) { - self.addEnsureSafeAssignContext(left); - } if (ast.computed) { right = self.nextId(); self.recurse(ast.property, right); self.getStringValue(right); - self.addEnsureSafeMemberName(right); if (create && create !== 1) { self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}')); } - expression = self.ensureSafeObject(self.computedMember(left, right)); + expression = self.computedMember(left, right); self.assign(intoId, expression); if (nameId) { nameId.computed = true; nameId.name = right; } } else { - ensureSafeMemberName(ast.property.name); if (create && create !== 1) { self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); } expression = self.nonComputedMember(left, ast.property.name); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { - expression = self.ensureSafeObject(expression); - } self.assign(intoId, expression); if (nameId) { nameId.computed = false; @@ -1122,21 +1003,16 @@ ASTCompiler.prototype = { args = []; self.recurse(ast.callee, right, left, function() { self.if_(self.notNull(right), function() { - self.addEnsureSafeFunction(right); forEach(ast.arguments, function(expr) { self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { - args.push(self.ensureSafeObject(argument)); + args.push(argument); }); }); if (left.name) { - if (!self.state.expensiveChecks) { - self.addEnsureSafeObject(left.context); - } expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')'; } else { expression = right + '(' + args.join(',') + ')'; } - expression = self.ensureSafeObject(expression); self.assign(intoId, expression); }, function() { self.assign(intoId, 'undefined'); @@ -1154,8 +1030,6 @@ ASTCompiler.prototype = { this.recurse(ast.left, undefined, left, function() { self.if_(self.notNull(left.context), function() { self.recurse(ast.right, right); - self.addEnsureSafeObject(self.member(left.context, left.name, left.computed)); - self.addEnsureSafeAssignContext(left.context); expression = self.member(left.context, left.name, left.computed) + ast.operator + right; self.assign(intoId, expression); recursionFn(intoId || expression); @@ -1303,42 +1177,10 @@ ASTCompiler.prototype = { return this.nonComputedMember(left, right); }, - addEnsureSafeObject: function(item) { - this.current().body.push(this.ensureSafeObject(item), ';'); - }, - - addEnsureSafeMemberName: function(item) { - this.current().body.push(this.ensureSafeMemberName(item), ';'); - }, - - addEnsureSafeFunction: function(item) { - this.current().body.push(this.ensureSafeFunction(item), ';'); - }, - - addEnsureSafeAssignContext: function(item) { - this.current().body.push(this.ensureSafeAssignContext(item), ';'); - }, - - ensureSafeObject: function(item) { - return 'ensureSafeObject(' + item + ',text)'; - }, - - ensureSafeMemberName: function(item) { - return 'ensureSafeMemberName(' + item + ',text)'; - }, - - ensureSafeFunction: function(item) { - return 'ensureSafeFunction(' + item + ',text)'; - }, - getStringValue: function(item) { this.assign(item, 'getStringValue(' + item + ')'); }, - ensureSafeAssignContext: function(item) { - return 'ensureSafeAssignContext(' + item + ',text)'; - }, - lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { var self = this; return function() { @@ -1390,11 +1232,10 @@ function ASTInterpreter(astBuilder, $filter) { } ASTInterpreter.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(expression) { var self = this; var ast = this.astBuilder.ast(expression); this.expression = expression; - this.expensiveChecks = expensiveChecks; findConstantAndWatchExpressions(ast, self.$filter); var assignable; var assign; @@ -1465,20 +1306,17 @@ ASTInterpreter.prototype = { context ); case AST.Identifier: - ensureSafeMemberName(ast.name, self.expression); return self.identifier(ast.name, - self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), context, create, self.expression); case AST.MemberExpression: left = this.recurse(ast.object, false, !!create); if (!ast.computed) { - ensureSafeMemberName(ast.property.name, self.expression); right = ast.property.name; } if (ast.computed) right = this.recurse(ast.property); return ast.computed ? this.computedMember(left, right, context, create, self.expression) : - this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression); + this.nonComputedMember(left, right, context, create, self.expression); case AST.CallExpression: args = []; forEach(ast.arguments, function(expr) { @@ -1499,13 +1337,11 @@ ASTInterpreter.prototype = { var rhs = right(scope, locals, assign, inputs); var value; if (rhs.value != null) { - ensureSafeObject(rhs.context, self.expression); - ensureSafeFunction(rhs.value, self.expression); var values = []; for (var i = 0; i < args.length; ++i) { - values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression)); + values.push(args[i](scope, locals, assign, inputs)); } - value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); + value = rhs.value.apply(rhs.context, values); } return context ? {value: value} : value; }; @@ -1515,8 +1351,6 @@ ASTInterpreter.prototype = { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); var rhs = right(scope, locals, assign, inputs); - ensureSafeObject(lhs.value, self.expression); - ensureSafeAssignContext(lhs.context); lhs.context[lhs.name] = rhs; return context ? {value: rhs} : rhs; }; @@ -1708,16 +1542,13 @@ ASTInterpreter.prototype = { value: function(value, context) { return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; }, - identifier: function(name, expensiveChecks, context, create, expression) { + identifier: function(name, context, create, expression) { return function(scope, locals, assign, inputs) { var base = locals && (name in locals) ? locals : scope; if (create && create !== 1 && base && !(base[name])) { base[name] = {}; } var value = base ? base[name] : undefined; - if (expensiveChecks) { - ensureSafeObject(value, expression); - } if (context) { return {context: base, name: name, value: value}; } else { @@ -1733,15 +1564,12 @@ ASTInterpreter.prototype = { if (lhs != null) { rhs = right(scope, locals, assign, inputs); rhs = getStringValue(rhs); - ensureSafeMemberName(rhs, expression); if (create && create !== 1) { - ensureSafeAssignContext(lhs); if (lhs && !(lhs[rhs])) { lhs[rhs] = {}; } } value = lhs[rhs]; - ensureSafeObject(value, expression); } if (context) { return {context: lhs, name: rhs, value: value}; @@ -1750,19 +1578,15 @@ ASTInterpreter.prototype = { } }; }, - nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { + nonComputedMember: function(left, right, context, create, expression) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); if (create && create !== 1) { - ensureSafeAssignContext(lhs); if (lhs && !(lhs[right])) { lhs[right] = {}; } } var value = lhs != null ? lhs[right] : undefined; - if (expensiveChecks || isPossiblyDangerousMemberName(right)) { - ensureSafeObject(value, expression); - } if (context) { return {context: lhs, name: right, value: value}; } else { @@ -1794,14 +1618,10 @@ Parser.prototype = { constructor: Parser, parse: function(text) { - return this.astCompiler.compile(text, this.options.expensiveChecks); + return this.astCompiler.compile(text); } }; -function isPossiblyDangerousMemberName(name) { - return name === 'constructor'; -} - function getValueOf(value) { return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); } @@ -1859,8 +1679,7 @@ function getValueOf(value) { * service. */ function $ParseProvider() { - var cacheDefault = createMap(); - var cacheExpensive = createMap(); + var cache = createMap(); var literals = { 'true': true, 'false': false, @@ -1918,37 +1737,20 @@ function $ParseProvider() { var noUnsafeEval = csp().noUnsafeEval; var $parseOptions = { csp: noUnsafeEval, - expensiveChecks: false, - literals: copy(literals), - isIdentifierStart: isFunction(identStart) && identStart, - isIdentifierContinue: isFunction(identContinue) && identContinue - }, - $parseOptionsExpensive = { - csp: noUnsafeEval, - expensiveChecks: true, literals: copy(literals), isIdentifierStart: isFunction(identStart) && identStart, isIdentifierContinue: isFunction(identContinue) && identContinue }; - var runningChecksEnabled = false; - - $parse.$$runningExpensiveChecks = function() { - return runningChecksEnabled; - }; - return $parse; - function $parse(exp, interceptorFn, expensiveChecks) { + function $parse(exp, interceptorFn) { var parsedExpression, oneTime, cacheKey; - expensiveChecks = expensiveChecks || runningChecksEnabled; - switch (typeof exp) { case 'string': exp = exp.trim(); cacheKey = exp; - var cache = (expensiveChecks ? cacheExpensive : cacheDefault); parsedExpression = cache[cacheKey]; if (!parsedExpression) { @@ -1956,9 +1758,8 @@ function $ParseProvider() { oneTime = true; exp = exp.substring(2); } - var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; - var lexer = new Lexer(parseOptions); - var parser = new Parser(lexer, $filter, parseOptions); + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); parsedExpression = parser.parse(exp); if (parsedExpression.constant) { parsedExpression.$$watchDelegate = constantWatchDelegate; @@ -1968,9 +1769,6 @@ function $ParseProvider() { } else if (parsedExpression.inputs) { parsedExpression.$$watchDelegate = inputsWatchDelegate; } - if (expensiveChecks) { - parsedExpression = expensiveChecksInterceptor(parsedExpression); - } cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); @@ -1983,30 +1781,6 @@ function $ParseProvider() { } } - function expensiveChecksInterceptor(fn) { - if (!fn) return fn; - expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate; - expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign); - expensiveCheckFn.constant = fn.constant; - expensiveCheckFn.literal = fn.literal; - for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) { - fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]); - } - expensiveCheckFn.inputs = fn.inputs; - - return expensiveCheckFn; - - function expensiveCheckFn(scope, locals, assign, inputs) { - var expensiveCheckOldValue = runningChecksEnabled; - runningChecksEnabled = true; - try { - return fn(scope, locals, assign, inputs); - } finally { - runningChecksEnabled = expensiveCheckOldValue; - } - } - } - function expressionInputDirtyCheck(newValue, oldValueOfValue) { if (newValue == null || oldValueOfValue == null) { // null/undefined diff --git a/test/ng/directive/ngEventDirsSpec.js b/test/ng/directive/ngEventDirsSpec.js index e2c2745f840e..d8288b828bbd 100644 --- a/test/ng/directive/ngEventDirsSpec.js +++ b/test/ng/directive/ngEventDirsSpec.js @@ -90,23 +90,13 @@ describe('event directives', function() { }); - describe('security', function() { + describe('DOM event object', function() { it('should allow access to the $event object', inject(function($rootScope, $compile) { var scope = $rootScope.$new(); element = $compile('')(scope); element.triggerHandler('click'); expect(scope.e.target).toBe(element[0]); })); - - it('should block access to DOM nodes (e.g. exposed via $event)', inject(function($rootScope, $compile) { - var scope = $rootScope.$new(); - element = $compile('')(scope); - expect(function() { - element.triggerHandler('click'); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is disallowed! ' + - 'Expression: e = $event.target'); - })); }); describe('blur', function() { diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index ea216a644c75..79d49340da46 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2422,770 +2422,6 @@ describe('parser', function() { })); - describe('sandboxing', function() { - describe('Function constructor', function() { - it('should not tranverse the Function constructor in the getter', function() { - expect(function() { - scope.$eval('{}.toString.constructor'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString.constructor'); - }); - - it('should not allow access to the Function prototype in the getter', function() { - expect(function() { - scope.$eval('toString.constructor.prototype'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: toString.constructor.prototype'); - }); - - it('should NOT allow access to Function constructor in getter', function() { - expect(function() { - scope.$eval('{}.toString.constructor("alert(1)")'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString.constructor("alert(1)")'); - }); - - it('should NOT allow access to Function constructor in setter', function() { - - expect(function() { - scope.$eval('{}.toString.constructor.a = 1'); - }).toThrowMinErr( - '$parse', 'isecfn','Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString.constructor.a = 1'); - - expect(function() { - scope.$eval('{}.toString["constructor"]["constructor"] = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString["constructor"]["constructor"] = 1'); - - scope.key1 = 'const'; - scope.key2 = 'ructor'; - expect(function() { - scope.$eval('{}.toString[key1 + key2].foo = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString[key1 + key2].foo = 1'); - - expect(function() { - scope.$eval('{}.toString["constructor"]["a"] = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString["constructor"]["a"] = 1'); - - scope.a = []; - expect(function() { - scope.$eval('a.toString.constructor = 1', scope); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: a.toString.constructor'); - }); - - it('should disallow traversing the Function object in a setter: E02', function() { - expect(function() { - // This expression by itself isn't dangerous. However, one can use this to - // automatically call an object (e.g. a Function object) when it is automatically - // toString'd/valueOf'd by setting the RHS to Function.prototype.call. - scope.$eval('hasOwnProperty.constructor.prototype.valueOf = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: hasOwnProperty.constructor.prototype.valueOf'); - }); - - it('should disallow passing the Function object as a parameter: E03', function() { - expect(function() { - // This expression constructs a function but does not execute it. It does lead the - // way to execute it if one can get the toString/valueOf of it to call the function. - scope.$eval('["a", "alert(1)"].sort(hasOwnProperty.constructor)'); - }).toThrow(); - }); - - it('should prevent exploit E01', function() { - // This is a tracking exploit. The two individual tests, it('should … : E02') and - // it('should … : E03') test for two parts to block this exploit. This exploit works - // as follows: - // - // • Array.sort takes a comparison function and passes it 2 parameters to compare. If - // the result is non-primitive, sort then invokes valueOf() on the result. - // • The Function object conveniently accepts two string arguments so we can use this - // to construct a function. However, this doesn't do much unless we can execute it. - // • We set the valueOf property on Function.prototype to Function.prototype.call. - // This causes the function that we constructed to be executed when sort calls - // .valueOf() on the result of the comparison. - expect(function() { - scope.$eval('' + - 'hasOwnProperty.constructor.prototype.valueOf=valueOf.call;' + - '["a","alert(1)"].sort(hasOwnProperty.constructor)'); - }).toThrow(); - }); - - it('should NOT allow access to Function constructor that has been aliased in getters', function() { - scope.foo = { 'bar': Function }; - expect(function() { - scope.$eval('foo["bar"]'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: foo["bar"]'); - }); - - it('should NOT allow access to Function constructor that has been aliased in setters', function() { - scope.foo = { 'bar': Function }; - expect(function() { - scope.$eval('foo["bar"] = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: foo["bar"] = 1'); - }); - - describe('expensiveChecks', function() { - it('should block access to window object even when aliased in getters', inject(function($parse, $window) { - scope.foo = {w: $window}; - // This isn't blocked for performance. - expect(scope.$eval($parse('foo.w'))).toBe($window); - // Event handlers use the more expensive path for better protection since they expose - // the $event object on the scope. - expect(function() { - scope.$eval($parse('foo.w', null, true)); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' + - 'Expression: foo.w'); - })); - - it('should block access to window object even when aliased in setters', inject(function($parse, $window) { - scope.foo = {w: $window}; - // This is blocked as it points to `window`. - expect(function() { - expect(scope.$eval($parse('foo.w = 1'))).toBe($window); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' + - 'Expression: foo.w = 1'); - // Event handlers use the more expensive path for better protection since they expose - // the $event object on the scope. - expect(function() { - scope.$eval($parse('foo.w = 1', null, true)); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' + - 'Expression: foo.w = 1'); - })); - - they('should propagate expensive checks when calling $prop', - ['foo.w && true', - '$eval("foo.w && true")', - 'this["$eval"]("foo.w && true")', - 'bar;$eval("foo.w && true")', - '$eval("foo.w && true");bar', - '$eval("foo.w && true", null, false)', - '$eval("foo");$eval("foo.w && true")', - '$eval("$eval(\\"foo.w && true\\")")', - '$eval("foo.e()")', - '$evalAsync("foo.w && true")', - 'this["$evalAsync"]("foo.w && true")', - 'bar;$evalAsync("foo.w && true")', - '$evalAsync("foo.w && true");bar', - '$evalAsync("foo.w && true", null, false)', - '$evalAsync("foo");$evalAsync("foo.w && true")', - '$evalAsync("$evalAsync(\\"foo.w && true\\")")', - '$evalAsync("foo.e()")', - '$evalAsync("$eval(\\"foo.w && true\\")")', - '$eval("$evalAsync(\\"foo.w && true\\")")', - '$watch("foo.w && true")', - '$watchCollection("foo.w && true", foo.f)', - '$watchGroup(["foo.w && true"])', - '$applyAsync("foo.w && true")'], function(expression) { - inject(function($parse, $window) { - scope.foo = { - w: $window, - bar: 'bar', - e: function() { scope.$eval('foo.w && true'); }, - f: function() {} - }; - expect($parse.$$runningExpensiveChecks()).toEqual(false); - expect(function() { - scope.$eval($parse(expression, null, true)); - scope.$digest(); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' + - 'Expression: foo.w && true'); - expect($parse.$$runningExpensiveChecks()).toEqual(false); - }); - }); - - they('should restore the state of $$runningExpensiveChecks when the expression $prop throws', - ['$eval("foo.t()")', - '$evalAsync("foo.t()", {foo: foo})'], function(expression) { - inject(function($parse, $window) { - scope.foo = { - t: function() { throw new Error(); } - }; - expect($parse.$$runningExpensiveChecks()).toEqual(false); - expect(function() { - scope.$eval($parse(expression, null, true)); - scope.$digest(); - }).toThrow(); - expect($parse.$$runningExpensiveChecks()).toEqual(false); - }); - }); - - it('should handle `inputs` when running with expensive checks', inject(function($parse) { - expect(function() { - scope.$watch($parse('a + b', null, true), noop); - scope.$digest(); - }).not.toThrow(); - })); - }); - }); - - describe('Function prototype functions', function() { - it('should NOT allow invocation to Function.call', function() { - scope.fn = Function.prototype.call; - - expect(function() { - scope.$eval('$eval.call()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: $eval.call()'); - - expect(function() { - scope.$eval('fn()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: fn()'); - }); - - it('should NOT allow invocation to Function.apply', function() { - scope.apply = Function.prototype.apply; - - expect(function() { - scope.$eval('$eval.apply()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: $eval.apply()'); - - expect(function() { - scope.$eval('apply()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: apply()'); - }); - - it('should NOT allow invocation to Function.bind', function() { - scope.bind = Function.prototype.bind; - - expect(function() { - scope.$eval('$eval.bind()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: $eval.bind()'); - - expect(function() { - scope.$eval('bind()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: bind()'); - }); - }); - - describe('Object constructor', function() { - - it('should NOT allow access to Object constructor that has been aliased in getters', function() { - scope.foo = { 'bar': Object }; - - expect(function() { - scope.$eval('foo.bar.keys(foo)'); - }).toThrowMinErr( - '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' + - 'Expression: foo.bar.keys(foo)'); - - expect(function() { - scope.$eval('foo["bar"]["keys"](foo)'); - }).toThrowMinErr( - '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' + - 'Expression: foo["bar"]["keys"](foo)'); - }); - - it('should NOT allow access to Object constructor that has been aliased in setters', function() { - scope.foo = { 'bar': Object }; - - expect(function() { - scope.$eval('foo.bar.keys(foo).bar = 1'); - }).toThrowMinErr( - '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' + - 'Expression: foo.bar.keys(foo).bar = 1'); - - expect(function() { - scope.$eval('foo["bar"]["keys"](foo).bar = 1'); - }).toThrowMinErr( - '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' + - 'Expression: foo["bar"]["keys"](foo).bar = 1'); - }); - }); - - describe('Window and $element/node', function() { - it('should NOT allow access to the Window or DOM when indexing', inject(function($window, $document) { - scope.wrap = {w: $window, d: $document}; - - expect(function() { - scope.$eval('wrap["w"]', scope); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + - 'disallowed! Expression: wrap["w"]'); - expect(function() { - scope.$eval('wrap["d"]', scope); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + - 'disallowed! Expression: wrap["d"]'); - expect(function() { - scope.$eval('wrap["w"] = 1', scope); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + - 'disallowed! Expression: wrap["w"] = 1'); - expect(function() { - scope.$eval('wrap["d"] = 1', scope); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + - 'disallowed! Expression: wrap["d"] = 1'); - })); - - it('should NOT allow access to the Window or DOM returned from a function', inject(function($window, $document) { - scope.getWin = valueFn($window); - scope.getDoc = valueFn($document); - - expect(function() { - scope.$eval('getWin()', scope); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + - 'disallowed! Expression: getWin()'); - expect(function() { - scope.$eval('getDoc()', scope); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + - 'disallowed! Expression: getDoc()'); - })); - - it('should NOT allow calling functions on Window or DOM', inject(function($window, $document) { - scope.a = {b: { win: $window, doc: $document }}; - expect(function() { - scope.$eval('a.b.win.alert(1)', scope); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + - 'disallowed! Expression: a.b.win.alert(1)'); - expect(function() { - scope.$eval('a.b.doc.on("click")', scope); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + - 'disallowed! Expression: a.b.doc.on("click")'); - })); - - // Issue #4805 - it('should NOT throw isecdom when referencing a Backbone Collection', function() { - // Backbone stuff is sort of hard to mock, if you have a better way of doing this, - // please fix this. - var fakeBackboneCollection = { - children: [{}, {}, {}], - find: function() {}, - on: function() {}, - off: function() {}, - bind: function() {} - }; - scope.backbone = fakeBackboneCollection; - expect(function() { scope.$eval('backbone'); }).not.toThrow(); - }); - - it('should NOT throw isecdom when referencing an array with node properties', function() { - var array = [1,2,3]; - array.on = array.attr = array.prop = array.bind = true; - scope.array = array; - expect(function() { scope.$eval('array'); }).not.toThrow(); - }); - }); - - describe('Disallowed fields', function() { - it('should NOT allow access or invocation of __defineGetter__', function() { - expect(function() { - scope.$eval('{}.__defineGetter__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__defineGetter__("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}["__defineGetter__"]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}["__defineGetter__"]("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - - scope.a = '__define'; - scope.b = 'Getter__'; - expect(function() { - scope.$eval('{}[a + b]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[a + b]("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - }); - - it('should NOT allow access or invocation of __defineSetter__', function() { - expect(function() { - scope.$eval('{}.__defineSetter__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__defineSetter__("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}["__defineSetter__"]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}["__defineSetter__"]("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - - scope.a = '__define'; - scope.b = 'Setter__'; - expect(function() { - scope.$eval('{}[a + b]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[a + b]("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - }); - - it('should NOT allow access or invocation of __lookupGetter__', function() { - expect(function() { - scope.$eval('{}.__lookupGetter__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__lookupGetter__("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}["__lookupGetter__"]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}["__lookupGetter__"]("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - - scope.a = '__lookup'; - scope.b = 'Getter__'; - expect(function() { - scope.$eval('{}[a + b]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[a + b]("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - }); - - it('should NOT allow access or invocation of __lookupSetter__', function() { - expect(function() { - scope.$eval('{}.__lookupSetter__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__lookupSetter__("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}["__lookupSetter__"]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}["__lookupSetter__"]("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - - scope.a = '__lookup'; - scope.b = 'Setter__'; - expect(function() { - scope.$eval('{}[a + b]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[a + b]("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - }); - - it('should NOT allow access to __proto__', function() { - expect(function() { - scope.$eval('__proto__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__proto__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__proto__.foo = 1'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}["__proto__"]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}["__proto__"].foo = 1'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}[["__proto__"]]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[["__proto__"]].foo = 1'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('0[["__proto__"]]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('0[["__proto__"]].foo = 1'); - }).toThrowMinErr('$parse', 'isecfld'); - - scope.a = '__pro'; - scope.b = 'to__'; - expect(function() { - scope.$eval('{}[a + b]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[a + b].foo = 1'); - }).toThrowMinErr('$parse', 'isecfld'); - }); - }); - - it('should prevent the exploit', function() { - expect(function() { - scope.$eval('(1)[{0: "__proto__", 1: "__proto__", 2: "__proto__", 3: "safe", length: 4, toString: [].pop}].foo = 1'); - }).toThrow(); - if (!msie || msie > 10) { - // eslint-disable-next-line no-proto - expect((1)['__proto__'].foo).toBeUndefined(); - } - }); - - it('should prevent the exploit', function() { - expect(function() { - scope.$eval('' + - ' "".sub.call.call(' + - '({})["constructor"].getOwnPropertyDescriptor("".sub.__proto__, "constructor").value,' + - 'null,' + - '"alert(1)"' + - ')()' + - ''); - }).toThrow(); - }); - - they('should prevent assigning in the context of the $prop constructor', { - Array: [[], '[]'], - Boolean: [true, '(true)'], - Number: [1, '(1)'], - String: ['string', '"string"'] - }, function(values) { - var thing = values[0]; - var expr = values[1]; - var constructorExpr = expr + '.constructor'; - - expect(function() { - scope.$eval(constructorExpr + '.join'); - }).not.toThrow(); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + constructorExpr + '.join'); - }).not.toThrow(); - expect(function() { - scope.$eval(constructorExpr + '.join = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.$eval(constructorExpr + '[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + constructorExpr + '; foo.join = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - - expect(function() { - scope.foo = thing; - scope.$eval('foo.constructor[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo.constructor[0] = ""', {foo: thing}); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.foo = thing.constructor; - scope.$eval('foo[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo[0] = ""', {foo: thing.constructor}); - }).toThrowMinErr('$parse', 'isecaf'); - }); - - they('should prevent assigning in the context of the $prop constructor', { - // These might throw different error (e.g. isecobj, isecfn), - // but still having them here for good measure - Function: [noop, '$eval'], - Object: [{}, '{}'] - }, function(values) { - var thing = values[0]; - var expr = values[1]; - var constructorExpr = expr + '.constructor'; - - expect(function() { - scope.$eval(constructorExpr + '.join'); - }).not.toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + constructorExpr + '.join'); - }).not.toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.$eval(constructorExpr + '.join = ""'); - }).toThrow(); - expect(function() { - scope.$eval(constructorExpr + '[0] = ""'); - }).toThrow(); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + constructorExpr + '; foo.join = ""'); - }).toThrow(); - - expect(function() { - scope.foo = thing; - scope.$eval('foo.constructor[0] = ""'); - }).toThrow(); - expect(function() { - delete scope.foo; - scope.$eval('foo.constructor[0] = ""', {foo: thing}); - }).toThrow(); - expect(function() { - scope.foo = thing.constructor; - scope.$eval('foo[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo[0] = ""', {foo: thing.constructor}); - }).toThrowMinErr('$parse', 'isecaf'); - }); - - it('should prevent assigning only in the context of an actual constructor', function() { - // foo.constructor is not a constructor. - expect(function() { - delete scope.foo; - scope.$eval('foo.constructor[0] = ""', {foo: {constructor: ''}}); - }).not.toThrow(); - - expect(function() { - scope.$eval('"a".constructor.prototype.charAt = [].join'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.$eval('"a".constructor.prototype.charCodeAt = [].concat'); - }).toThrowMinErr('$parse', 'isecaf'); - }); - - they('should prevent assigning in the context of the $prop constructor prototype', { - Array: [[], '[]'], - Boolean: [true, '(true)'], - Number: [1, '(1)'], - String: ['string', '"string"'] - }, function(values) { - var thing = values[0]; - var expr = values[1]; - var constructorExpr = expr + '.constructor'; - var prototypeExpr = constructorExpr + '.prototype'; - - expect(function() { - scope.$eval(prototypeExpr + '.boin'); - }).not.toThrow(); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + prototypeExpr + '.boin'); - }).not.toThrow(); - expect(function() { - scope.$eval(prototypeExpr + '.boin = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.$eval(prototypeExpr + '[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + constructorExpr + '; foo.prototype.boin = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + prototypeExpr + '; foo.boin = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - - expect(function() { - scope.foo = thing.constructor; - scope.$eval('foo.prototype[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo.prototype[0] = ""', {foo: thing.constructor}); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.foo = thing.constructor.prototype; - scope.$eval('foo[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo[0] = ""', {foo: thing.constructor.prototype}); - }).toThrowMinErr('$parse', 'isecaf'); - }); - - they('should prevent assigning in the context of a constructor prototype', { - // These might throw different error (e.g. isecobj, isecfn), - // but still having them here for good measure - Function: [noop, '$eval'], - Object: [{}, '{}'] - }, function(values) { - var thing = values[0]; - var expr = values[1]; - var constructorExpr = expr + '.constructor'; - var prototypeExpr = constructorExpr + '.prototype'; - - expect(function() { - scope.$eval(prototypeExpr + '.boin'); - }).not.toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + prototypeExpr + '.boin'); - }).not.toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.$eval(prototypeExpr + '.boin = ""'); - }).toThrow(); - expect(function() { - scope.$eval(prototypeExpr + '[0] = ""'); - }).toThrow(); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + constructorExpr + '; foo.prototype.boin = ""'); - }).toThrow(); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + prototypeExpr + '; foo.boin = ""'); - }).toThrow(); - - expect(function() { - scope.foo = thing.constructor; - scope.$eval('foo.prototype[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo.prototype[0] = ""', {foo: thing.constructor}); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.foo = thing.constructor.prototype; - scope.$eval('foo[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo[0] = ""', {foo: thing.constructor.prototype}); - }).toThrowMinErr('$parse', 'isecaf'); - }); - - it('should prevent assigning only in the context of an actual prototype', function() { - // foo.constructor.prototype is not a constructor prototype. - expect(function() { - delete scope.foo; - scope.$eval('foo.constructor.prototype[0] = ""', {foo: {constructor: {prototype: ''}}}); - }).not.toThrow(); - }); - }); - it('should call the function from the received instance and not from a new one', function() { var n = 0; scope.fn = function() { From 32aa7e7395527624119e3917c54ee43b4d219301 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 15 Sep 2016 11:59:42 +0100 Subject: [PATCH 0010/1014] fix(ngTransclude): use fallback content if only whitespace is provided If the transcluded content is only whitespace then we should use the fallback content instead. This allows more flexibility in formatting your HTML. Closes #15077 Closes #15140 BREAKING CHANGE: Previously whitespace only transclusion would be treated as the transclusion being "not empty", which meant that fallback content was not used in that case. Now if you only provide whitespace as the transclusion content, it will be assumed to be empty and the fallback content will be used instead. If you really do want whitespace then you can force it to be used by adding a comment to the whitespace. --- src/ng/directive/ngTransclude.js | 15 +++++++-- test/ng/compileSpec.js | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/ng/directive/ngTransclude.js b/src/ng/directive/ngTransclude.js index f19c251fc3b6..4f48f5ee0bbf 100644 --- a/src/ng/directive/ngTransclude.js +++ b/src/ng/directive/ngTransclude.js @@ -13,8 +13,8 @@ * * If the transcluded content is not empty (i.e. contains one or more DOM nodes, including whitespace text nodes), any existing * content of this element will be removed before the transcluded content is inserted. - * If the transcluded content is empty, the existing content is left intact. This lets you provide fallback content in the case - * that no transcluded content is provided. + * If the transcluded content is empty (or only whitespace), the existing content is left intact. This lets you provide fallback + * content in the case that no transcluded content is provided. * * @element ANY * @@ -195,7 +195,7 @@ var ngTranscludeDirective = ['$compile', function($compile) { } function ngTranscludeCloneAttachFn(clone, transcludedScope) { - if (clone.length) { + if (clone.length && notWhitespace(clone)) { $element.append(clone); } else { useFallbackContent(); @@ -212,6 +212,15 @@ var ngTranscludeDirective = ['$compile', function($compile) { $element.append(clone); }); } + + function notWhitespace(nodes) { + for (var i = 0, ii = nodes.length; i < ii; i++) { + var node = nodes[i]; + if (node.nodeType !== NODE_TYPE_TEXT || node.nodeValue.trim()) { + return true; + } + } + } }; } }; diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 70112d14a82f..a9659524fd21 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -8728,6 +8728,60 @@ describe('$compile', function() { }); }); + it('should compile and link the fallback content if only whitespace transcluded content is provided', function() { + var linkSpy = jasmine.createSpy('postlink'); + + module(function() { + directive('inner', function() { + return { + restrict: 'E', + template: 'old stuff! ', + link: linkSpy + }; + }); + + directive('trans', function() { + return { + transclude: true, + template: '
' + }; + }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
\n \n
')($rootScope); + $rootScope.$apply(); + expect(sortedHtml(element.html())).toEqual('
old stuff!
'); + expect(linkSpy).toHaveBeenCalled(); + }); + }); + + it('should not link the fallback content if only whitespace and comments are provided as transclude content', function() { + var linkSpy = jasmine.createSpy('postlink'); + + module(function() { + directive('inner', function() { + return { + restrict: 'E', + template: 'old stuff! ', + link: linkSpy + }; + }); + + directive('trans', function() { + return { + transclude: true, + template: '
' + }; + }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
\n \n
')($rootScope); + $rootScope.$apply(); + expect(sortedHtml(element.html())).toEqual('
\n \n
'); + expect(linkSpy).not.toHaveBeenCalled(); + }); + }); + it('should compile and link the fallback content if an optional transclusion slot is not provided', function() { var linkSpy = jasmine.createSpy('postlink'); From 52bf2bd11e9070807762bad1a7ff048a67f24b75 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 12 Sep 2016 17:30:38 +0300 Subject: [PATCH 0011/1014] refactor(ngOptions): access `copy()` directly (`angular.copy` --> `copy`) --- src/ng/directive/ngOptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js index 9b58f9b985fd..db00e4cee7bc 100644 --- a/src/ng/directive/ngOptions.js +++ b/src/ng/directive/ngOptions.js @@ -397,7 +397,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, getViewValueFromOption: function(option) { // If the viewValue could be an object that may be mutated by the application, // we need to make a copy and not return the reference to the value on the option. - return trackBy ? angular.copy(option.viewValue) : option.viewValue; + return trackBy ? copy(option.viewValue) : option.viewValue; } }; } From a1bdffa12f82e838dee5492956b380df7e54cdf9 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 12 Sep 2016 16:45:32 +0300 Subject: [PATCH 0012/1014] fix($compile): do not overwrite values set in `$onInit()` for `<`-bound literals See #15118 for more details. Fixes #15118 Closes #15123 --- src/ng/compile.js | 7 +++++-- test/ng/compileSpec.js | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 631ddc67cedd..1d2ae28e1b07 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -3506,18 +3506,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (optional && !attrs[attrName]) break; parentGet = $parse(attrs[attrName]); + var deepWatch = parentGet.literal; var initialValue = destination[scopeName] = parentGet(scope); initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) { if (oldValue === newValue) { - if (oldValue === initialValue) return; + if (oldValue === initialValue || (deepWatch && equals(oldValue, initialValue))) { + return; + } oldValue = initialValue; } recordChanges(scopeName, newValue, oldValue); destination[scopeName] = newValue; - }, parentGet.literal); + }, deepWatch); removeWatchCollection.push(removeWatch); break; diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index a9659524fd21..2784d16ccf7c 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -5524,7 +5524,7 @@ describe('$compile', function() { expect($rootScope.name).toEqual('outer'); expect(component.input).toEqual('$onInit'); - $rootScope.$apply(); + $rootScope.$digest(); expect($rootScope.name).toEqual('outer'); expect(component.input).toEqual('$onInit'); @@ -5537,6 +5537,37 @@ describe('$compile', function() { }); }); + it('should not update isolate again after $onInit if outer is a literal', function() { + module('owComponentTest'); + inject(function() { + $rootScope.name = 'outer'; + compile(''); + + expect(component.input).toEqual('$onInit'); + + // No outer change + $rootScope.$apply('name = "outer"'); + expect(component.input).toEqual('$onInit'); + + // Outer change + $rootScope.$apply('name = "re-outer"'); + expect(component.input).toEqual(['re-outer']); + + expect(log).toEqual([ + 'constructor', + [ + '$onChanges', + jasmine.objectContaining({currentValue: ['outer']}) + ], + '$onInit', + [ + '$onChanges', + jasmine.objectContaining({previousValue: ['outer'], currentValue: ['re-outer']}) + ] + ]); + }); + }); + it('should update isolate again after $onInit if outer has changed (before initial watchAction call)', function() { module('owComponentTest'); inject(function() { From 16dccea8873b06285d4ec6eb3bb8e96ccbd3b64e Mon Sep 17 00:00:00 2001 From: thorn0 Date: Thu, 8 Sep 2016 17:54:13 +0300 Subject: [PATCH 0013/1014] fix($compile): `bindToController` should work without `controllerAs` Fixes #15088 Closes #15110 --- docs/content/error/$compile/noident.ngdoc | 71 ------- src/ng/compile.js | 28 +-- test/ng/compileSpec.js | 237 +++++++++------------- 3 files changed, 106 insertions(+), 230 deletions(-) delete mode 100644 docs/content/error/$compile/noident.ngdoc diff --git a/docs/content/error/$compile/noident.ngdoc b/docs/content/error/$compile/noident.ngdoc deleted file mode 100644 index 9770a94585e1..000000000000 --- a/docs/content/error/$compile/noident.ngdoc +++ /dev/null @@ -1,71 +0,0 @@ -@ngdoc error -@name $compile:noident -@fullName Controller identifier is required. -@description - -When using the `bindToController` feature of AngularJS, a directive is required -to have a Controller identifier, which is initialized in scope with the value of -the controller instance. This can be supplied using the "controllerAs" property -of the directive object, or alternatively by adding " as IDENTIFIER" to the controller -name. - -For example, the following directives are valid: - -```js -// OKAY, because controller is a string with an identifier component. -directive("okay", function() { - return { - bindToController: true, - controller: "myCtrl as $ctrl", - scope: { - text: "@text" - } - }; -}); - - -// OKAY, because the directive uses the controllerAs property to override -// the controller identifier. -directive("okay2", function() { - return { - bindToController: true, - controllerAs: "$ctrl", - controller: function() { - - }, - scope: { - text: "@text" - } - }; -}); -``` - -While the following are invalid: - -```js -// BAD, because the controller property is a string with no identifier. -directive("bad", function() { - return { - bindToController: true, - controller: "noIdentCtrl", - scope: { - text: "@text" - } - }; -}); - - -// BAD because the controller is not a string (therefore has no identifier), -// and there is no controllerAs property. -directive("bad2", function() { - return { - bindToController: true, - controller: function noControllerAs() { - - }, - scope: { - text: "@text" - } - }; -}); -``` diff --git a/src/ng/compile.js b/src/ng/compile.js index 1d2ae28e1b07..df611d315c3d 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -361,9 +361,7 @@ * * #### `bindToController` * This property is used to bind scope properties directly to the controller. It can be either - * `true` or an object hash with the same format as the `scope` property. Additionally, a controller - * alias must be set, either by using `controllerAs: 'myAlias'` or by specifying the alias in the controller - * definition: `controller: 'myCtrl as myAlias'`. + * `true` or an object hash with the same format as the `scope` property. * * When an isolate scope is used for a directive (see above), `bindToController: true` will * allow a component to have its properties bound to the controller, rather than to scope. @@ -1028,20 +1026,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { bindings.bindToController = parseIsolateBindings(directive.bindToController, directiveName, true); } - if (isObject(bindings.bindToController)) { - var controller = directive.controller; - var controllerAs = directive.controllerAs; - if (!controller) { - // There is no controller, there may or may not be a controllerAs property - throw $compileMinErr('noctrl', - 'Cannot bind to controller without directive \'{0}\'s controller.', - directiveName); - } else if (!identifierForController(controller, controllerAs)) { - // There is a controller, but no identifier or controllerAs property - throw $compileMinErr('noident', - 'Cannot bind to controller without identifier for directive \'{0}\'.', - directiveName); - } + if (bindings.bindToController && !directive.controller) { + // There is no controller + throw $compileMinErr('noctrl', + 'Cannot bind to controller without directive \'{0}\'s controller.', + directiveName); } return bindings; } @@ -2710,7 +2699,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var bindings = controllerDirective.$$bindings.bindToController; if (preAssignBindingsEnabled) { - if (controller.identifier && bindings) { + if (bindings) { controller.bindingInfo = initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } else { @@ -3413,8 +3402,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } - // Set up $watches for isolate scope and controller bindings. This process - // only occurs for isolate scopes and new scopes with controllerAs. + // Set up $watches for isolate scope and controller bindings. function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) { var removeWatchCollection = []; var initialChanges = {}; diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 2784d16ccf7c..492741a4496a 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -6205,154 +6205,113 @@ describe('$compile', function() { }); - it('should throw noident when missing controllerAs directive property', function() { - module(function($compileProvider) { - $compileProvider.directive('noIdent', valueFn({ - templateUrl: 'test.html', - scope: { - 'data': '=dirData', - 'oneway': '')($rootScope); - }).toThrowMinErr('$compile', 'noident', - 'Cannot bind to controller without identifier for directive \'noIdent\'.'); - }); - }); - - - it('should throw noident when missing controller identifier', function() { - module(function($compileProvider, $controllerProvider) { - $controllerProvider.register('myCtrl', function() {}); - $compileProvider.directive('noIdent', valueFn({ - templateUrl: 'test.html', - scope: { - 'data': '=dirData', - 'oneway': '')($rootScope); - }).toThrowMinErr('$compile', 'noident', - 'Cannot bind to controller without identifier for directive \'noIdent\'.'); - }); - }); + controllerAs: 'myCtrl' + }], + scopeOptions = [{ + description: 'isolate scope', + scope: {} + }, { + description: 'new scope', + scope: true + }, { + description: 'no scope', + scope: false + }], - it('should bind to controller via object notation (isolate scope)', function() { - var controllerCalled = false; - module(function($compileProvider, $controllerProvider) { - $controllerProvider.register('myCtrl', function() { - this.check = function() { - expect(this.data).toEqualData({ - 'foo': 'bar', - 'baz': 'biz' - }); - expect(this.oneway).toEqualData({ - 'foo': 'bar', - 'baz': 'biz' - }); - expect(this.str).toBe('Hello, world!'); - expect(this.fn()).toBe('called!'); - }; - controllerCalled = true; - if (preAssignBindingsEnabled) { - this.check(); - } else { - this.$onInit = this.check; - } - }); - $compileProvider.directive('fooDir', valueFn({ - templateUrl: 'test.html', - bindToController: { - 'data': '=dirData', - 'oneway': 'isolate

'); - $rootScope.fn = valueFn('called!'); - $rootScope.whom = 'world'; - $rootScope.remoteData = { - 'foo': 'bar', - 'baz': 'biz' - }; - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(controllerCalled).toBe(true); - }); - }); + templateOptions = [{ + description: 'inline template', + template: '

template

' + }, { + description: 'templateUrl setting', + templateUrl: 'test.html' + }, { + description: 'no template' + }]; + forEach(controllerOptions, function(controllerOption) { + forEach(scopeOptions, function(scopeOption) { + forEach(templateOptions, function(templateOption) { + + var description = [], + ddo = { + bindToController: { + 'data': '=dirData', + 'oneway': 'template

'); + $rootScope.fn = valueFn('called!'); + $rootScope.whom = 'world'; + $rootScope.remoteData = { + 'foo': 'bar', + 'baz': 'biz' + }; + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(controllerCalled).toBe(true); + if (ddo.controllerAs || ddo.controller.indexOf(' as ') !== -1) { + if (ddo.scope) { + expect($rootScope.myCtrl).toBeUndefined(); + } else { + // The controller identifier was added to the containing scope. + expect($rootScope.myCtrl).toBeDefined(); + } + } + }); }); - expect(this.str).toBe('Hello, world!'); - expect(this.fn()).toBe('called!'); - }; - controllerCalled = true; - if (preAssignBindingsEnabled) { - this.check(); - } else { - this.$onInit = this.check; - } + + }); }); - $compileProvider.directive('fooDir', valueFn({ - templateUrl: 'test.html', - bindToController: { - 'data': '=dirData', - 'oneway': 'isolate

'); - $rootScope.fn = valueFn('called!'); - $rootScope.whom = 'world'; - $rootScope.remoteData = { - 'foo': 'bar', - 'baz': 'biz' - }; - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(controllerCalled).toBe(true); }); + }); From bb8e955a02093b90567677820aefebb8b48f1c4f Mon Sep 17 00:00:00 2001 From: Stepan Suvorov Date: Sun, 18 Sep 2016 18:57:51 +0200 Subject: [PATCH 0014/1014] docs(cacheFactory): remove `ng-include` practice from docs Generally we don't use `ngInclude` any more, so this commit updates the example snippet to use component instead. Closes #15153 --- src/ng/cacheFactory.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ng/cacheFactory.js b/src/ng/cacheFactory.js index 84e91c9883bd..3a8f5f5c7e67 100644 --- a/src/ng/cacheFactory.js +++ b/src/ng/cacheFactory.js @@ -385,12 +385,14 @@ function $CacheFactoryProvider() { * }); * ``` * - * To retrieve the template later, simply use it in your HTML: - * ```html - *
+ * To retrieve the template later, simply use it in your component: + * ```js + * myApp.component('myComponent', { + * template: 'templateId.html' + * }); * ``` * - * or get it via Javascript: + * or get it via $templateCache service: * ```js * $templateCache.get('templateId.html') * ``` From e1e2fe1c089d6cfac5b8c8517d29e6b7c431ea78 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 19 Sep 2016 17:57:31 +0300 Subject: [PATCH 0015/1014] docs($templateCache): fix typo (template --> templateCache) --- src/ng/cacheFactory.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ng/cacheFactory.js b/src/ng/cacheFactory.js index 3a8f5f5c7e67..c4735d2738b9 100644 --- a/src/ng/cacheFactory.js +++ b/src/ng/cacheFactory.js @@ -388,11 +388,11 @@ function $CacheFactoryProvider() { * To retrieve the template later, simply use it in your component: * ```js * myApp.component('myComponent', { - * template: 'templateId.html' + * templateUrl: 'templateId.html' * }); * ``` * - * or get it via $templateCache service: + * or get it via the `$templateCache` service: * ```js * $templateCache.get('templateId.html') * ``` From f1cc58c7d282d48a9b23b90f1cc5e5b2bc9f296e Mon Sep 17 00:00:00 2001 From: Matt Gilson Date: Mon, 19 Sep 2016 08:36:25 -0700 Subject: [PATCH 0016/1014] docs(angular.toJson): add missing param type Reference: [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify). Closes #15156 --- src/Angular.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Angular.js b/src/Angular.js index ad68d3e128d5..dced06cc28b2 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1223,7 +1223,7 @@ function toJsonReplacer(key, value) { * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be * stripped since angular uses this notation internally. * - * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. + * @param {Object|Array|Date|string|number|boolean} obj Input to be serialized into JSON. * @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace. * If set to an integer, the JSON output will contain that many spaces per indentation. * @returns {string|undefined} JSON-ified string representing `obj`. From b59bc0b01ddddbe42310d113dafe127d4e71511c Mon Sep 17 00:00:00 2001 From: davidcigital Date: Thu, 15 Sep 2016 13:59:55 +0100 Subject: [PATCH 0017/1014] docs(ngCsp): update explanation of CSP rules and how they affect Angular Update the description of CSP, mainly regarding `unsafe-eval` and `unsafe-inline`. The way it was presented previously was slightly misleading as it indicated that these were rules forbidding certain things, when in fact it's a keyword in the CSP that disables the very rule that was described. The updated text clarifies this better. Closes #15142 --- src/ng/directive/ngCsp.js | 44 ++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/ng/directive/ngCsp.js b/src/ng/directive/ngCsp.js index f0303ef8a844..38a6e67b2b1e 100644 --- a/src/ng/directive/ngCsp.js +++ b/src/ng/directive/ngCsp.js @@ -7,28 +7,34 @@ * @element html * @description * - * Angular has some features that can break certain + * Angular has some features that can conflict with certain restrictions that are applied when using * [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules. * - * If you intend to implement these rules then you must tell Angular not to use these features. + * If you intend to implement CSP with these rules then you must tell Angular not to use these + * features. * * This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps. * * - * The following rules affect Angular: + * The following default rules in CSP affect Angular: * - * * `unsafe-eval`: this rule forbids apps to use `eval` or `Function(string)` generated functions - * (among other things). Angular makes use of this in the {@link $parse} service to provide a 30% - * increase in the speed of evaluating Angular expressions. + * * The use of `eval()`, `Function(string)` and similar functions to dynamically create and execute + * code from strings is forbidden. Angular makes use of this in the {@link $parse} service to + * provide a 30% increase in the speed of evaluating Angular expressions. (This CSP rule can be + * disabled with the CSP keyword `unsafe-eval`, but it is generally not recommended as it would + * weaken the protections offered by CSP.) * - * * `unsafe-inline`: this rule forbids apps from inject custom styles into the document. Angular - * makes use of this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}). - * To make these directives work when a CSP rule is blocking inline styles, you must link to the - * `angular-csp.css` in your HTML manually. + * * The use of inline resources, such as inline ` + + \ No newline at end of file diff --git a/test/e2e/tests/input-hidden.spec.js b/test/e2e/tests/input-hidden.spec.js new file mode 100644 index 000000000000..ef2669f0f64a --- /dev/null +++ b/test/e2e/tests/input-hidden.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('hidden thingy', function() { + it('should pass', function() { + + loadFixture('input-hidden'); + expect(element(by.css('input')).getAttribute('value')).toEqual(''); + + element(by.css('button')).click(); + expect(element(by.css('input')).getAttribute('value')).toEqual('{{ 7 * 6 }}'); + + loadFixture('sample'); + browser.driver.executeScript('history.back()'); + var expectedValue = browser.params.browser === 'safari' ? '{{ 7 * 6 }}' : ''; + expect(element(by.css('input')).getAttribute('value')).toEqual(expectedValue); + }); +}); From 8d394de91fd96f87afdbb277520b111232ec4bdb Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Oct 2016 23:07:52 +0100 Subject: [PATCH 0061/1014] docs(CHANGELOG): add 1.2.31 and 1.4.13 release info --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b3efbe3c208..99fa22865764 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ + +# 1.4.13 croaking-elderweed (2016-10-10) + +## Bug Fixes +- **input:** ensure that hidden input values are correct after history back + ([693d1334](https://github.com/angular/angular.js/commit/693d1334566f78987f5a361a100db4f889f35abd) + + + +# 1.2.31 barking-moustache (2016-10-10) + +## Bug Fixes +- **input:** ensure that hidden input values are correct after history back + ([7ec663fc](https://github.com/angular/angular.js/commit/7ec663fc708aa7a9a9ce62d2306f24d7a733a86d) + + + +# 1.4.12 + +*Invalid release* + + # 1.5.8 arbitrary-fallbacks (2016-07-22) From ec83b04df1f38f37066c2ce6cf791405af73a9b3 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 11 Oct 2016 13:28:56 +0100 Subject: [PATCH 0062/1014] revert:fix(input): ensure that hidden input values are correct after history.back This reverts commit 298f8c4d13fbaa104845619e5518cb03c2aad05b. There was a regression in angular-material that relied upon the input directive having `link.pre` property. --- src/ng/directive/input.js | 15 ++++++--------- test/e2e/fixtures/input-hidden/index.html | 10 ---------- test/e2e/tests/input-hidden.spec.js | 17 ----------------- 3 files changed, 6 insertions(+), 36 deletions(-) delete mode 100644 test/e2e/fixtures/input-hidden/index.html delete mode 100644 test/e2e/tests/input-hidden.spec.js diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index c253908651d8..7e26ca16f6d1 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -2002,16 +2002,13 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', return { restrict: 'E', require: ['?ngModel'], - compile: function(tElement, tAttr) { - if (lowercase(tAttr.type) === 'hidden') tAttr.$set('autocomplete', 'off'); - return { - pre: function(scope, element, attr, ctrls) { - if (ctrls[0]) { - (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, - $browser, $filter, $parse); - } + link: { + pre: function(scope, element, attr, ctrls) { + if (ctrls[0]) { + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, + $browser, $filter, $parse); } - }; + } } }; }]; diff --git a/test/e2e/fixtures/input-hidden/index.html b/test/e2e/fixtures/input-hidden/index.html deleted file mode 100644 index 881639100ad9..000000000000 --- a/test/e2e/fixtures/input-hidden/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -
- - -
- - - \ No newline at end of file diff --git a/test/e2e/tests/input-hidden.spec.js b/test/e2e/tests/input-hidden.spec.js deleted file mode 100644 index ef2669f0f64a..000000000000 --- a/test/e2e/tests/input-hidden.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -describe('hidden thingy', function() { - it('should pass', function() { - - loadFixture('input-hidden'); - expect(element(by.css('input')).getAttribute('value')).toEqual(''); - - element(by.css('button')).click(); - expect(element(by.css('input')).getAttribute('value')).toEqual('{{ 7 * 6 }}'); - - loadFixture('sample'); - browser.driver.executeScript('history.back()'); - var expectedValue = browser.params.browser === 'safari' ? '{{ 7 * 6 }}' : ''; - expect(element(by.css('input')).getAttribute('value')).toEqual(expectedValue); - }); -}); From 606ea5d23ee8bbb8b7ee238bf07ce4c72de7eaac Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 11 Oct 2016 13:33:42 +0100 Subject: [PATCH 0063/1014] fix($compile): ensure that hidden input values are correct after history.back Due to the nature of some browser's PageCache/BFCache, returning to an Angular app sometimes causes `input[hidden]` elements to retain the last value that was stored before the page was navigated away from previously. This is particularly problematic if the input has an interpolated value. E.g. `` since when the browser returns, instead of the original interpolation template, the HTML contains the previous value ``. This commit instructs the browser not to attempt to reinstate the previous value when navigating back in history by setting `autocomplete="off"` on the hidden input element element. --- src/ng/compile.js | 12 +++++++++++- test/e2e/fixtures/input-hidden/index.html | 10 ++++++++++ test/e2e/tests/input-hidden.spec.js | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 test/e2e/fixtures/input-hidden/index.html create mode 100644 test/e2e/tests/input-hidden.spec.js diff --git a/src/ng/compile.js b/src/ng/compile.js index f26cfa22a053..6c9cf801f8bc 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -2115,13 +2115,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var nodeType = node.nodeType, attrsMap = attrs.$attr, match, + nodeName, className; switch (nodeType) { case NODE_TYPE_ELEMENT: /* Element */ + + nodeName = nodeName_(node); + // use the node name: addDirective(directives, - directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); + directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective); // iterate over the attributes for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, @@ -2163,6 +2167,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { attrEndName); } + if (nodeName === 'input' && node.getAttribute('type') === 'hidden') { + // Hidden input elements can have strange behaviour when navigating back to the page + // This tells the browser not to try to cache and reinstate previous values + node.setAttribute('autocomplete', 'off'); + } + // use class as directive if (!cssClassDirectivesEnabled) break; className = node.className; diff --git a/test/e2e/fixtures/input-hidden/index.html b/test/e2e/fixtures/input-hidden/index.html new file mode 100644 index 000000000000..881639100ad9 --- /dev/null +++ b/test/e2e/fixtures/input-hidden/index.html @@ -0,0 +1,10 @@ + + + +
+ + +
+ + + \ No newline at end of file diff --git a/test/e2e/tests/input-hidden.spec.js b/test/e2e/tests/input-hidden.spec.js new file mode 100644 index 000000000000..ef2669f0f64a --- /dev/null +++ b/test/e2e/tests/input-hidden.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('hidden thingy', function() { + it('should pass', function() { + + loadFixture('input-hidden'); + expect(element(by.css('input')).getAttribute('value')).toEqual(''); + + element(by.css('button')).click(); + expect(element(by.css('input')).getAttribute('value')).toEqual('{{ 7 * 6 }}'); + + loadFixture('sample'); + browser.driver.executeScript('history.back()'); + var expectedValue = browser.params.browser === 'safari' ? '{{ 7 * 6 }}' : ''; + expect(element(by.css('input')).getAttribute('value')).toEqual(expectedValue); + }); +}); From 4f44e018948c45bfb07f0170de4f703d22778d71 Mon Sep 17 00:00:00 2001 From: mohamed amr Date: Sun, 11 Sep 2016 20:31:14 +0200 Subject: [PATCH 0064/1014] fix($parse): treat falsy values as defined in assignment expressions Closes #14990 Closes #14994 --- src/ng/parse.js | 12 +++++--- test/ng/parseSpec.js | 66 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 12682eaff889..3e2661e16f6d 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -944,7 +944,7 @@ ASTCompiler.prototype = { self.if_(self.stage === 'inputs' || 's', function() { if (create && create !== 1) { self.if_( - self.not(self.nonComputedMember('s', ast.name)), + self.isNull(self.nonComputedMember('s', ast.name)), self.lazyAssign(self.nonComputedMember('s', ast.name), '{}')); } self.assign(intoId, self.nonComputedMember('s', ast.name)); @@ -973,7 +973,7 @@ ASTCompiler.prototype = { } } else { if (create && create !== 1) { - self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); + self.if_(self.isNull(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); } expression = self.nonComputedMember(left, ast.property.name); self.assign(intoId, expression); @@ -1155,6 +1155,10 @@ ASTCompiler.prototype = { return '!(' + expression + ')'; }, + isNull: function(expression) { + return expression + '==null'; + }, + notNull: function(expression) { return expression + '!=null'; }, @@ -1546,7 +1550,7 @@ ASTInterpreter.prototype = { identifier: function(name, context, create, expression) { return function(scope, locals, assign, inputs) { var base = locals && (name in locals) ? locals : scope; - if (create && create !== 1 && base && !(base[name])) { + if (create && create !== 1 && base && base[name] == null) { base[name] = {}; } var value = base ? base[name] : undefined; @@ -1583,7 +1587,7 @@ ASTInterpreter.prototype = { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); if (create && create !== 1) { - if (lhs && !(lhs[right])) { + if (lhs && lhs[right] == null) { lhs[right] = {}; } } diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 9b4171077c03..6af4671b264f 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -3155,6 +3155,72 @@ describe('parser', function() { expect(isFunction(s.toString)).toBe(true); expect(l.toString).toBe(1); })); + + it('should overwrite undefined / null scope properties when assigning', inject(function($parse) { + var scope; + + scope = {}; + $parse('a.b = 1')(scope); + $parse('c["d"] = 2')(scope); + expect(scope).toEqual({a: {b: 1}, c: {d: 2}}); + + scope = {a: {}}; + $parse('a.b.c = 1')(scope); + $parse('a.c["d"] = 2')(scope); + expect(scope).toEqual({a: {b: {c: 1}, c: {d: 2}}}); + + scope = {a: undefined, c: undefined}; + $parse('a.b = 1')(scope); + $parse('c["d"] = 2')(scope); + expect(scope).toEqual({a: {b: 1}, c: {d: 2}}); + + scope = {a: {b: undefined, c: undefined}}; + $parse('a.b.c = 1')(scope); + $parse('a.c["d"] = 2')(scope); + expect(scope).toEqual({a: {b: {c: 1}, c: {d: 2}}}); + + scope = {a: null, c: null}; + $parse('a.b = 1')(scope); + $parse('c["d"] = 2')(scope); + expect(scope).toEqual({a: {b: 1}, c: {d: 2}}); + + scope = {a: {b: null, c: null}}; + $parse('a.b.c = 1')(scope); + $parse('a.c["d"] = 2')(scope); + expect(scope).toEqual({a: {b: {c: 1}, c: {d: 2}}}); + })); + + they('should not overwrite $prop scope properties when assigning', [0, false, '', NaN], + function(falsyValue) { + inject(function($parse) { + var scope; + + scope = {a: falsyValue, c: falsyValue}; + tryParseAndIgnoreException('a.b = 1'); + tryParseAndIgnoreException('c["d"] = 2'); + expect(scope).toEqual({a: falsyValue, c: falsyValue}); + + scope = {a: {b: falsyValue, c: falsyValue}}; + tryParseAndIgnoreException('a.b.c = 1'); + tryParseAndIgnoreException('a.c["d"] = 2'); + expect(scope).toEqual({a: {b: falsyValue, c: falsyValue}}); + + // Helpers + // + // Normally assigning property on a primitive should throw exception in strict mode + // and silently fail in non-strict mode, IE seems to always have the non-strict-mode behavior, + // so if we try to use 'expect(function() {$parse('a.b=1')({a:false});).toThrow()' for testing + // the test will fail in case of IE because it will not throw exception, and if we just use + // '$parse('a.b=1')({a:false})' the test will fail because it will throw exception in case of Chrome + // so we use tryParseAndIgnoreException helper to catch the exception silently for all cases. + // + function tryParseAndIgnoreException(expression) { + try { + $parse(expression)(scope); + } catch (error) {/* ignore exception */} + } + }); + }); }); describe('literal', function() { From 369fb7f4f73664bcdab0350701552d8bef6f605e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Sun, 9 Oct 2016 23:51:53 +0200 Subject: [PATCH 0065/1014] feat(jqLite): implement jqLite(f) as alias to jqLite(document).ready(f) jQuery has supported this form for a long time. As of jQuery 3.0 this form is the preferred one and all others are deprecated so jqLite(f) is now also supported. All internal invocations of jqLite(document).ready(f) (& equivalent) have been replaced by jqLite(f). Tests for these methods have been added as jqLite#ready had no explicit tests so far. --- docs/content/guide/bootstrap.ngdoc | 4 +-- src/angular.bind.js | 7 ++--- src/angular.suffix | 2 +- src/jqLite.js | 45 +++++++++++++++++------------- src/ngScenario/angular.suffix | 2 +- test/e2e/fixtures/ready/index.html | 13 +++++++++ test/e2e/fixtures/ready/script.js | 32 +++++++++++++++++++++ test/e2e/tests/ready.spec.js | 25 +++++++++++++++++ 8 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 test/e2e/fixtures/ready/index.html create mode 100644 test/e2e/fixtures/ready/script.js create mode 100644 test/e2e/tests/ready.spec.js diff --git a/docs/content/guide/bootstrap.ngdoc b/docs/content/guide/bootstrap.ngdoc index f0a896baa1d4..15d4ae9a13af 100644 --- a/docs/content/guide/bootstrap.ngdoc +++ b/docs/content/guide/bootstrap.ngdoc @@ -115,7 +115,7 @@ Here is an example of manually initializing Angular: $scope.greetMe = 'World'; }]); - angular.element(document).ready(function() { + angular.element(function() { angular.bootstrap(document, ['myApp']); }); @@ -167,4 +167,4 @@ until `angular.resumeBootstrap()` is called. `angular.resumeBootstrap()` takes an optional array of modules that should be added to the original list of modules that the app was -about to be bootstrapped with. \ No newline at end of file +about to be bootstrapped with. diff --git a/src/angular.bind.js b/src/angular.bind.js index 45bcdecb7247..e23e8915a7e9 100644 --- a/src/angular.bind.js +++ b/src/angular.bind.js @@ -1,12 +1,11 @@ if (window.angular.bootstrap) { - //AngularJS is already loaded, so we can return here... + // AngularJS is already loaded, so we can return here... if (window.console) { console.log('WARNING: Tried to load angular more than once.'); } return; } -//try to bind to jquery now so that one can write jqLite(document).ready() -//but we will rebind on bootstrap again. +// try to bind to jquery now so that one can write jqLite(fn) +// but we will rebind on bootstrap again. bindJQuery(); - diff --git a/src/angular.suffix b/src/angular.suffix index 9cdd66db8a4b..fddb3d072ebe 100644 --- a/src/angular.suffix +++ b/src/angular.suffix @@ -1,4 +1,4 @@ - jqLite(window.document).ready(function() { + jqLite(function() { angularInit(window.document, bootstrap); }); diff --git a/src/jqLite.js b/src/jqLite.js index 92da9ef6fa65..fba31e738b8c 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -288,6 +288,8 @@ function JQLite(element) { if (argIsString) { jqLiteAddNodes(this, jqLiteParseHTML(element)); + } else if (isFunction(element)) { + jqLiteReady(element); } else { jqLiteAddNodes(this, element); } @@ -519,29 +521,34 @@ function jqLiteDocumentLoaded(action, win) { } } +function jqLiteReady(fn) { + var fired = false; + + function trigger() { + if (fired) return; + fired = true; + fn(); + } + + // check if document is already loaded + if (window.document.readyState === 'complete') { + window.setTimeout(fn); + } else { + // We can not use jqLite since we are not done loading and jQuery could be loaded later. + + // Works for modern browsers and IE9 + window.document.addEventListener('DOMContentLoaded', trigger); + + // Fallback to window.onload for others + window.addEventListener('load', trigger); + } +} + ////////////////////////////////////////// // Functions which are declared directly. ////////////////////////////////////////// var JQLitePrototype = JQLite.prototype = { - ready: function(fn) { - var fired = false; - - function trigger() { - if (fired) return; - fired = true; - fn(); - } - - // check if document is already loaded - if (window.document.readyState === 'complete') { - window.setTimeout(trigger); - } else { - this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - // eslint-disable-next-line new-cap - JQLite(window).on('load', trigger); // fallback to window.onload for others - } - }, + ready: jqLiteReady, toString: function() { var value = []; forEach(this, function(e) { value.push('' + e);}); diff --git a/src/ngScenario/angular.suffix b/src/ngScenario/angular.suffix index bb2bd6af2848..5c89e61036f3 100644 --- a/src/ngScenario/angular.suffix +++ b/src/ngScenario/angular.suffix @@ -14,7 +14,7 @@ angular.forEach(script.attributes, function(attr) { }); if (config.autotest) { - JQLite(window.document).ready(function() { + JQLite(function() { angular.scenario.setUpAndRun(config); }); } diff --git a/test/e2e/fixtures/ready/index.html b/test/e2e/fixtures/ready/index.html new file mode 100644 index 000000000000..732ce0690dc3 --- /dev/null +++ b/test/e2e/fixtures/ready/index.html @@ -0,0 +1,13 @@ + + + + {{beforeReady}} + {{afterReady}} + {{afterReadySync}} + {{afterReadyMethod}} + {{afterReadyMethodSync}} + + +
This div is loaded after scripts.
+ + diff --git a/test/e2e/fixtures/ready/script.js b/test/e2e/fixtures/ready/script.js new file mode 100644 index 000000000000..77713e4606cd --- /dev/null +++ b/test/e2e/fixtures/ready/script.js @@ -0,0 +1,32 @@ +'use strict'; + +var beforeReady; +(function() { + var divAfterScripts = window.document.getElementById('div-after-scripts'); + beforeReady = divAfterScripts && divAfterScripts.textContent; +})(); + +var afterReady; +angular.element(function() { + var divAfterScripts = window.document.getElementById('div-after-scripts'); + afterReady = divAfterScripts && divAfterScripts.textContent; +}); + +var afterReadyMethod; +angular.element(window.document).ready(function() { + var divAfterScripts = window.document.getElementById('div-after-scripts'); + afterReadyMethod = divAfterScripts && divAfterScripts.textContent; +}); + +var afterReadySync = afterReady; +var afterReadyMethodSync = afterReadyMethod; + +angular + .module('test', []) + .run(function($rootScope) { + $rootScope.beforeReady = beforeReady; + $rootScope.afterReady = afterReady; + $rootScope.afterReadySync = afterReadySync; + $rootScope.afterReadyMethod = afterReadyMethod; + $rootScope.afterReadyMethodSync = afterReadyMethodSync; + }); diff --git a/test/e2e/tests/ready.spec.js b/test/e2e/tests/ready.spec.js new file mode 100644 index 000000000000..d2576b8c6734 --- /dev/null +++ b/test/e2e/tests/ready.spec.js @@ -0,0 +1,25 @@ +'use strict'; + +describe('Firing a callback on ready', function() { + it('should not have the div available immediately', function() { + loadFixture('ready'); + expect(element(by.className('before-ready')).getText()) + .toBe(''); + }); + + it('should wait for document ready', function() { + loadFixture('ready'); + expect(element(by.className('after-ready')).getText()) + .toBe('This div is loaded after scripts.'); + expect(element(by.className('after-ready-method')).getText()) + .toBe('This div is loaded after scripts.'); + }); + + it('should be asynchronous', function() { + loadFixture('ready'); + expect(element(by.className('after-ready-sync')).getText()) + .toBe(''); + expect(element(by.className('after-ready-method-sync')).getText()) + .toBe(''); + }); +}); From 8e82bf51b13c80bcba7195f649cff2445b9d641c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Sun, 9 Oct 2016 23:53:00 +0200 Subject: [PATCH 0066/1014] refactor(jqLite): deprecate jqLite#ready Use jqLite(fn) instead of jqLite(document).ready(fn). --- src/jqLite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jqLite.js b/src/jqLite.js index fba31e738b8c..0f87488bb2ab 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -76,7 +76,7 @@ * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors * - [`prepend()`](http://api.jquery.com/prepend/) * - [`prop()`](http://api.jquery.com/prop/) - * - [`ready()`](http://api.jquery.com/ready/) + * - [`ready()`](http://api.jquery.com/ready/) (_deprecated_, use `angular.element(callback)` instead of `angular.element(document).ready(callback)`) * - [`remove()`](http://api.jquery.com/remove/) * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - Does not support multiple attributes * - [`removeClass()`](http://api.jquery.com/removeClass/) - Does not support a function as first argument From e008df6c8c1f2f7741618f5e57f3ebcbeebd9a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 12 Oct 2016 14:38:31 +0200 Subject: [PATCH 0067/1014] docs(jqLite): remove the removal plan info for bind/unbind We're not going to remove the aliases before jQuery does. --- src/jqLite.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 0f87488bb2ab..8a6410d781bd 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -56,7 +56,7 @@ * - [`after()`](http://api.jquery.com/after/) * - [`append()`](http://api.jquery.com/append/) * - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters - * - [`bind()`](http://api.jquery.com/bind/) (_deprecated_ - to be removed in 1.7.0, use [`on()`](http://api.jquery.com/on/)) - Does not support namespaces, selectors or eventData + * - [`bind()`](http://api.jquery.com/bind/) (_deprecated_, use [`on()`](http://api.jquery.com/on/)) - Does not support namespaces, selectors or eventData * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) @@ -85,7 +85,7 @@ * - [`text()`](http://api.jquery.com/text/) * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - Does not support a function as first argument * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers - * - [`unbind()`](http://api.jquery.com/unbind/) (_deprecated_ - to be removed in 1.7.0, use [`off()`](http://api.jquery.com/off/)) - Does not support namespaces or event object as parameter + * - [`unbind()`](http://api.jquery.com/unbind/) (_deprecated_, use [`off()`](http://api.jquery.com/off/)) - Does not support namespaces or event object as parameter * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * From beab3baec3728cd4034df4bbccdf52af2b7d8b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 12 Oct 2016 14:41:53 +0200 Subject: [PATCH 0068/1014] chore(jqLite): remove the ready handlers instead of setting a flag This change aligns jqLite with the jQuery implementation. Closes #15237 --- src/jqLite.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 8a6410d781bd..df8565e90ad2 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -522,11 +522,9 @@ function jqLiteDocumentLoaded(action, win) { } function jqLiteReady(fn) { - var fired = false; - function trigger() { - if (fired) return; - fired = true; + window.document.removeEventListener('DOMContentLoaded', trigger); + window.removeEventListener('load', trigger); fn(); } From 73050cdda04675bfa6705dc841ddbbb6919eb048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Wed, 12 Oct 2016 14:23:14 +0200 Subject: [PATCH 0069/1014] fix(jqLite): align jqLite camelCasing logic with JQuery jqLite needs camelCase for it's css method; it should only convert one dash followed by a lowercase letter to an uppercase one; it shouldn't touch underscores, colons or collapse multiple dashes into one. This is behavior of jQuery 3 as well. Also, jqLite's css camelCasing logic was put in a separate function and refactored: now the properties starting from an uppercase letter are used by default (i.e. Webkit, not webkit) and the only exception is for the -ms- prefix that is converted to ms, not Ms. This makes the logic clearer as we're just always changing a dash followed by a lowercase letter by an uppercase one; this is also how it works in jQuery. The camelCasing for the $compile and $sce services retains the previous behaviour. Ref #15126 Fix #7744 BREAKING CHANGE: before, when Angular was used without jQuery, the key passed to the css method was more heavily camelCased; now only a single (!) hyphen followed by a lowercase letter is getting transformed. This also affects APIs that rely on the css method, like ngStyle. If you use Angular with jQuery, it already behaved in this way so no changes are needed on your part. To migrate the code follow the example below: Before: HTML: // All five versions used to be equivalent.
JS: // All five versions used to be equivalent. elem.css('background_color', 'blue'); elem.css('background:color', 'blue'); elem.css('background-color', 'blue'); elem.css('background--color', 'blue'); elem.css('backgroundColor', 'blue'); // All five versions used to be equivalent. var bgColor = elem.css('background_color'); var bgColor = elem.css('background:color'); var bgColor = elem.css('background-color'); var bgColor = elem.css('background--color'); var bgColor = elem.css('backgroundColor'); After: HTML: // Previous five versions are no longer equivalent but these two still are.
JS: // Previous five versions are no longer equivalent but these two still are. elem.css('background-color', 'blue'); elem.css('backgroundColor', 'blue'); // Previous five versions are no longer equivalent but these two still are. var bgColor = elem.css('background-color'); var bgColor = elem.css('backgroundColor'); --- src/.eslintrc.json | 2 +- src/jqLite.js | 31 +++++++---- src/ng/compile.js | 6 +- src/ng/sce.js | 13 ++++- test/.eslintrc.json | 3 +- test/jqLiteSpec.js | 133 ++++++++++++++++++++++++++++++++++++++++---- 6 files changed, 159 insertions(+), 29 deletions(-) diff --git a/src/.eslintrc.json b/src/.eslintrc.json index a5d959535803..6022ac25baa9 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -123,7 +123,7 @@ "BOOLEAN_ATTR": false, "ALIASED_ATTR": false, "jqNextId": false, - "camelCase": false, + "fnCamelCaseReplace": false, "jqLitePatchJQueryRemove": false, "JQLite": false, "jqLiteClone": false, diff --git a/src/jqLite.js b/src/jqLite.js index df8565e90ad2..4b5c58304fce 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -136,22 +136,31 @@ JQLite._data = function(node) { function jqNextId() { return ++jqId; } -var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g; -var MOZ_HACK_REGEXP = /^moz([A-Z])/; +var DASH_LOWERCASE_REGEXP = /-([a-z])/g; +var MS_HACK_REGEXP = /^-ms-/; var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' }; var jqLiteMinErr = minErr('jqLite'); /** - * Converts snake_case to camelCase. - * Also there is special case for Moz prefix starting with upper case letter. + * Converts kebab-case to camelCase. + * There is also a special case for the ms prefix starting with a lowercase letter. * @param name Name to normalize */ -function camelCase(name) { - return name. - replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { - return offset ? letter.toUpperCase() : letter; - }). - replace(MOZ_HACK_REGEXP, 'Moz$1'); +function cssKebabToCamel(name) { + return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-')); +} + +function fnCamelCaseReplace(all, letter) { + return letter.toUpperCase(); +} + +/** + * Converts kebab-case to camelCase. + * @param name Name to normalize + */ +function kebabToCamel(name) { + return name + .replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace); } var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/; @@ -633,7 +642,7 @@ forEach({ hasClass: jqLiteHasClass, css: function(element, name, value) { - name = camelCase(name); + name = cssKebabToCamel(name); if (isDefined(value)) { element.style[name] = value; diff --git a/src/ng/compile.js b/src/ng/compile.js index 6c9cf801f8bc..bc1cae5bfca6 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -3590,12 +3590,16 @@ SimpleChange.prototype.isFirstChange = function() { return this.previousValue == var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i; +var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; + /** * Converts all accepted directives format into proper directive name. * @param name Name to normalize */ function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); + return name + .replace(PREFIX_REGEXP, '') + .replace(SPECIAL_CHARS_REGEXP, fnCamelCaseReplace); } /** diff --git a/src/ng/sce.js b/src/ng/sce.js index 074c13d84579..0201656c6ea7 100644 --- a/src/ng/sce.js +++ b/src/ng/sce.js @@ -27,6 +27,13 @@ var SCE_CONTEXTS = { // Helper functions follow. +var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g; + +function snakeToCamel(name) { + return name + .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace); +} + function adjustMatcher(matcher) { if (matcher === 'self') { return matcher; @@ -1054,13 +1061,13 @@ function $SceProvider() { forEach(SCE_CONTEXTS, function(enumValue, name) { var lName = lowercase(name); - sce[camelCase('parse_as_' + lName)] = function(expr) { + sce[snakeToCamel('parse_as_' + lName)] = function(expr) { return parse(enumValue, expr); }; - sce[camelCase('get_trusted_' + lName)] = function(value) { + sce[snakeToCamel('get_trusted_' + lName)] = function(value) { return getTrusted(enumValue, value); }; - sce[camelCase('trust_as_' + lName)] = function(value) { + sce[snakeToCamel('trust_as_' + lName)] = function(value) { return trustAs(enumValue, value); }; }); diff --git a/test/.eslintrc.json b/test/.eslintrc.json index ca882335b492..026e7f2ed18f 100644 --- a/test/.eslintrc.json +++ b/test/.eslintrc.json @@ -118,7 +118,8 @@ /* jqLite.js */ "BOOLEAN_ATTR": false, "jqNextId": false, - "camelCase": false, + "kebabToCamel": false, + "fnCamelCaseReplace": false, "jqLitePatchJQueryRemove": false, "JQLite": false, "jqLiteClone": false, diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index f4a3fbfc16c5..0b54c233d43c 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -1055,6 +1055,105 @@ describe('jqLite', function() { expect(jqA.css('z-index')).toBeOneOf('7', 7); expect(jqA.css('zIndex')).toBeOneOf('7', 7); }); + + it('should leave non-dashed strings alone', function() { + var jqA = jqLite(a); + + jqA.css('foo', 'foo'); + jqA.css('fooBar', 'bar'); + + expect(a.style.foo).toBe('foo'); + expect(a.style.fooBar).toBe('bar'); + }); + + it('should convert dash-separated strings to camelCase', function() { + var jqA = jqLite(a); + + jqA.css('foo-bar', 'foo'); + jqA.css('foo-bar-baz', 'bar'); + jqA.css('foo:bar_baz', 'baz'); + + expect(a.style.fooBar).toBe('foo'); + expect(a.style.fooBarBaz).toBe('bar'); + expect(a.style['foo:bar_baz']).toBe('baz'); + }); + + it('should convert leading dashes followed by a lowercase letter', function() { + var jqA = jqLite(a); + + jqA.css('-foo-bar', 'foo'); + + expect(a.style.FooBar).toBe('foo'); + }); + + it('should not convert slashes followed by a non-letter', function() { + // jQuery 2.x had different behavior; skip the test. + if (isJQuery2x()) return; + + var jqA = jqLite(a); + + jqA.css('foo-42- -a-B', 'foo'); + + expect(a.style['foo-42- A-B']).toBe('foo'); + }); + + it('should convert the -ms- prefix to ms instead of Ms', function() { + var jqA = jqLite(a); + + jqA.css('-ms-foo-bar', 'foo'); + jqA.css('-moz-foo-bar', 'bar'); + jqA.css('-webkit-foo-bar', 'baz'); + + expect(a.style.msFooBar).toBe('foo'); + expect(a.style.MozFooBar).toBe('bar'); + expect(a.style.WebkitFooBar).toBe('baz'); + }); + + it('should not collapse sequences of dashes', function() { + var jqA = jqLite(a); + + jqA.css('foo---bar-baz--qaz', 'foo'); + + expect(a.style['foo--BarBaz-Qaz']).toBe('foo'); + }); + + + it('should read vendor prefixes with the special -ms- exception', function() { + // jQuery uses getComputedStyle() in a css getter so these tests would fail there. + if (!_jqLiteMode) return; + + var jqA = jqLite(a); + + a.style.WebkitFooBar = 'webkit-uppercase'; + a.style.webkitFooBar = 'webkit-lowercase'; + + a.style.MozFooBaz = 'moz-uppercase'; + a.style.mozFooBaz = 'moz-lowercase'; + + a.style.MsFooQaz = 'ms-uppercase'; + a.style.msFooQaz = 'ms-lowercase'; + + expect(jqA.css('-webkit-foo-bar')).toBe('webkit-uppercase'); + expect(jqA.css('-moz-foo-baz')).toBe('moz-uppercase'); + expect(jqA.css('-ms-foo-qaz')).toBe('ms-lowercase'); + }); + + it('should write vendor prefixes with the special -ms- exception', function() { + var jqA = jqLite(a); + + jqA.css('-webkit-foo-bar', 'webkit'); + jqA.css('-moz-foo-baz', 'moz'); + jqA.css('-ms-foo-qaz', 'ms'); + + expect(a.style.WebkitFooBar).toBe('webkit'); + expect(a.style.webkitFooBar).not.toBeDefined(); + + expect(a.style.MozFooBaz).toBe('moz'); + expect(a.style.mozFooBaz).not.toBeDefined(); + + expect(a.style.MsFooQaz).not.toBeDefined(); + expect(a.style.msFooQaz).toBe('ms'); + }); }); @@ -2267,25 +2366,35 @@ describe('jqLite', function() { }); - describe('camelCase', function() { + describe('kebabToCamel', function() { it('should leave non-dashed strings alone', function() { - expect(camelCase('foo')).toBe('foo'); - expect(camelCase('')).toBe(''); - expect(camelCase('fooBar')).toBe('fooBar'); + expect(kebabToCamel('foo')).toBe('foo'); + expect(kebabToCamel('')).toBe(''); + expect(kebabToCamel('fooBar')).toBe('fooBar'); + }); + + it('should convert dash-separated strings to camelCase', function() { + expect(kebabToCamel('foo-bar')).toBe('fooBar'); + expect(kebabToCamel('foo-bar-baz')).toBe('fooBarBaz'); + expect(kebabToCamel('foo:bar_baz')).toBe('foo:bar_baz'); }); + it('should convert leading dashes followed by a lowercase letter', function() { + expect(kebabToCamel('-foo-bar')).toBe('FooBar'); + }); - it('should covert dash-separated strings to camelCase', function() { - expect(camelCase('foo-bar')).toBe('fooBar'); - expect(camelCase('foo-bar-baz')).toBe('fooBarBaz'); - expect(camelCase('foo:bar_baz')).toBe('fooBarBaz'); + it('should not convert dashes followed by a non-letter', function() { + expect(kebabToCamel('foo-42- -a-B')).toBe('foo-42- A-B'); }); + it('should not convert browser specific css properties in a special way', function() { + expect(kebabToCamel('-ms-foo-bar')).toBe('MsFooBar'); + expect(kebabToCamel('-moz-foo-bar')).toBe('MozFooBar'); + expect(kebabToCamel('-webkit-foo-bar')).toBe('WebkitFooBar'); + }); - it('should covert browser specific css properties', function() { - expect(camelCase('-moz-foo-bar')).toBe('MozFooBar'); - expect(camelCase('-webkit-foo-bar')).toBe('webkitFooBar'); - expect(camelCase('-webkit-foo-bar')).toBe('webkitFooBar'); + it('should not collapse sequences of dashes', function() { + expect(kebabToCamel('foo---bar-baz--qaz')).toBe('foo--BarBaz-Qaz'); }); }); From fc0c11db845d53061430b7f05e773dcb3fb5b860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Mon, 10 Oct 2016 01:56:28 +0200 Subject: [PATCH 0070/1014] fix(jqLite): camelCase keys in jqLite#data This change aligns jqLite with jQuery 3. The relevant bit of jQuery code is https://github.com/jquery/jquery/blob/3.1.1/src/data/Data.js Close #15126 BREAKING CHANGE: Previously, keys passed to the data method were left untouched. Now they are internally camelCased similarly to how jQuery handles it, i.e. only single (!) hyphens followed by a lowercase letter get converted to an uppercase letter. This means keys `a-b` and `aB` represent the same data piece; writing to one of them will also be reflected if you ask for the other one. If you use Angular with jQuery, it already behaved in this way so no changes are required on your part. To migrate the code follow the examples below: BEFORE: /* 1 */ elem.data('my-key', 2); elem.data('myKey', 3); /* 2 */ elem.data('foo-bar', 42); elem.data()['foo-bar']; // 42 elem.data()['fooBar']; // undefined /* 3 */ elem.data()['foo-bar'] = 1; elem.data()['fooBar'] = 2; elem.data()['foo-bar']; // 1 AFTER: /* 1 */ // Rename one of the keys as they would now map to the same data slot. elem.data('my-key', 2); elem.data('my-key2', 3); /* 2 */ elem.data('foo-bar', 42); elem.data()['foo-bar']; // undefined elem.data()['fooBar']; // 42 /* 3 */ elem.data()['foo-bar'] = 1; elem.data()['fooBar'] = 2; elem.data()['foo-bar']; // 2 --- src/jqLite.js | 9 ++++++--- test/jqLiteSpec.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 4b5c58304fce..e882407be63f 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -394,6 +394,7 @@ function jqLiteExpandoStore(element, createIfNecessary) { function jqLiteData(element, key, value) { if (jqLiteAcceptsData(element)) { + var prop; var isSimpleSetter = isDefined(value); var isSimpleGetter = !isSimpleSetter && key && !isObject(key); @@ -402,16 +403,18 @@ function jqLiteData(element, key, value) { var data = expandoStore && expandoStore.data; if (isSimpleSetter) { // data('key', value) - data[key] = value; + data[kebabToCamel(key)] = value; } else { if (massGetter) { // data() return data; } else { if (isSimpleGetter) { // data('key') // don't force creation of expandoStore if it doesn't exist yet - return data && data[key]; + return data && data[kebabToCamel(key)]; } else { // mass-setter: data({key1: val1, key2: val2}) - extend(data, key); + for (prop in key) { + data[kebabToCamel(prop)] = key[prop]; + } } } } diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index 0b54c233d43c..886ca7c2cd42 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -594,6 +594,39 @@ describe('jqLite', function() { }).not.toThrow(); }); }); + + describe('camelCasing keys', function() { + // jQuery 2.x has different behavior; skip the tests. + if (isJQuery2x()) return; + + it('should camelCase the key in a setter', function() { + var element = jqLite(a); + + element.data('a-B-c-d-42--e', 'z-x'); + expect(element.data()).toEqual({'a-BCD-42-E': 'z-x'}); + }); + + it('should camelCase the key in a getter', function() { + var element = jqLite(a); + + element.data()['a-BCD-42-E'] = 'x-c'; + expect(element.data('a-B-c-d-42--e')).toBe('x-c'); + }); + + it('should camelCase the key in a mass setter', function() { + var element = jqLite(a); + + element.data({'a-B-c-d-42--e': 'c-v', 'r-t-v': 42}); + expect(element.data()).toEqual({'a-BCD-42-E': 'c-v', 'rTV': 42}); + }); + + it('should ignore non-camelCase keys in the data in a getter', function() { + var element = jqLite(a); + + element.data()['a-b'] = 'b-n'; + expect(element.data('a-b')).toBe(undefined); + }); + }); }); From c22615cbfbaa7d1712e79b6bf2ace6eb41313bac Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 14 Oct 2016 11:39:21 +0200 Subject: [PATCH 0071/1014] refactor(compileSpec): make tests consistent PR (#15141) --- test/ng/compileSpec.js | 206 ++++++++++++++--------------------------- 1 file changed, 72 insertions(+), 134 deletions(-) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 1786d3cd603b..544baf8a74ca 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -1201,41 +1201,52 @@ describe('$compile', function() { }); }); - it('should fail if replacing and template doesn\'t have a single root element', function() { - module(function() { - directive('noRootElem', function() { - return { - replace: true, - template: 'dada' - }; - }); - directive('multiRootElem', function() { - return { - replace: true, - template: '
' - }; - }); - directive('singleRootWithWhiteSpace', function() { + describe('replace and not exactly one root element', function() { + var templateVar; + + beforeEach(module(function() { + directive('template', function() { return { replace: true, - template: '
\n' + template: function() { + return templateVar; + } }; }); - }); + })); - inject(function($compile) { - expect(function() { - $compile('

'); - }).toThrowMinErr('$compile', 'tplrt', 'Template for directive \'noRootElem\' must have exactly one root element. '); + they('should throw if: $prop', + { + 'no root element': 'dada', + 'multiple root elements': '
' + }, function(directiveTemplate) { - expect(function() { - $compile('

'); - }).toThrowMinErr('$compile', 'tplrt', 'Template for directive \'multiRootElem\' must have exactly one root element. '); + inject(function($compile) { + templateVar = directiveTemplate; + expect(function() { + $compile('

'); + }).toThrowMinErr('$compile', 'tplrt', + 'Template for directive \'template\' must have exactly one root element.' + ); + }); + }); - // ws is ok - expect(function() { - $compile('

'); - }).not.toThrow(); + they('should not throw if the root element is accompanied by: $prop', + { + 'whitespace': '
Hello World!
\n', + 'comments': '
Hello World!
\n', + 'comments + whitespace': '
Hello World!
\n' + }, function(directiveTemplate) { + + inject(function($compile, $rootScope) { + templateVar = directiveTemplate; + var element; + expect(function() { + element = $compile('

')($rootScope); + }).not.toThrow(); + expect(element.length).toBe(1); + expect(element.text()).toBe('Hello World!'); + }); }); }); @@ -1348,38 +1359,6 @@ describe('$compile', function() { }); } - it('should ignore comment nodes when replacing with a template', function() { - module(function() { - directive('replaceWithComments', valueFn({ - replace: true, - template: '

Hello, world!

' - })); - }); - inject(function($compile, $rootScope) { - expect(function() { - element = $compile('
')($rootScope); - }).not.toThrow(); - expect(element.find('p').length).toBe(1); - expect(element.find('p').text()).toBe('Hello, world!'); - }); - }); - - it('should ignore whitespace betwee comment and root node when replacing with a template', function() { - module(function() { - directive('replaceWithWhitespace', valueFn({ - replace: true, - template: '

Hello, world!

' - })); - }); - inject(function($compile, $rootScope) { - expect(function() { - element = $compile('
')($rootScope); - }).not.toThrow(); - expect(element.find('p').length).toBe(1); - expect(element.find('p').text()).toBe('Hello, world!'); - }); - }); - it('should keep prototype properties on directive', function() { module(function() { function DirectiveClass() { @@ -2078,10 +2057,9 @@ describe('$compile', function() { } )); + describe('replace and not exactly one root element', function() { - it('should fail if replacing and template doesn\'t have a single root element', function() { - module(function($exceptionHandlerProvider) { - $exceptionHandlerProvider.mode('log'); + beforeEach(module(function() { directive('template', function() { return { @@ -2089,46 +2067,45 @@ describe('$compile', function() { templateUrl: 'template.html' }; }); - }); + })); - inject(function($compile, $templateCache, $rootScope, $exceptionHandler) { - // no root element - $templateCache.put('template.html', 'dada'); - $compile('

'); - $rootScope.$digest(); - expect($exceptionHandler.errors.pop()).toEqualMinErr('$compile', 'tplrt', - 'Template for directive \'template\' must have exactly one root element. ' + - 'template.html'); + they('should throw if: $prop', + { + 'no root element': 'dada', + 'multiple root elements': '
' + }, function(directiveTemplate) { - // multi root - $templateCache.put('template.html', '
'); - $compile('

'); - $rootScope.$digest(); - expect($exceptionHandler.errors.pop()).toEqualMinErr('$compile', 'tplrt', - 'Template for directive \'template\' must have exactly one root element. ' + - 'template.html'); + inject(function($compile, $templateCache, $rootScope, $exceptionHandler) { + $templateCache.put('template.html', directiveTemplate); + $compile('

')($rootScope); + $rootScope.$digest(); - // ws is ok - $templateCache.put('template.html', '
\n'); - $compile('

'); - $rootScope.$apply(); - expect($exceptionHandler.errors).toEqual([]); + expect($exceptionHandler.errors.pop()).toEqualMinErr('$compile', 'tplrt', + 'Template for directive \'template\' must have exactly one root element. ' + + 'template.html' + ); + }); + }); - // comments are ok - $templateCache.put('template.html', '
\n'); - $compile('

'); - $rootScope.$apply(); - expect($exceptionHandler.errors).toEqual([]); + they('should not throw if the root element is accompanied by: $prop', + { + 'whitespace': '
Hello World!
\n', + 'comments': '
Hello World!
\n', + 'comments + whitespace': '
Hello World!
\n' + }, function(directiveTemplate) { - // white space around comments is ok - $templateCache.put('template.html', '
\n'); - $compile('

'); - $rootScope.$apply(); - expect($exceptionHandler.errors).toEqual([]); + inject(function($compile, $templateCache, $rootScope) { + $templateCache.put('template.html', directiveTemplate); + element = $compile('

')($rootScope); + expect(function() { + $rootScope.$digest(); + }).not.toThrow(); + expect(element.length).toBe(1); + expect(element.text()).toBe('Hello World!'); + }); }); }); - it('should resume delayed compilation without duplicates when in a repeater', function() { // this is a test for a regression // scope creation, isolate watcher setup, controller instantiation, etc should happen @@ -2317,45 +2294,6 @@ describe('$compile', function() { }); } - it('should ignore comment nodes when replacing with a templateUrl', function() { - module(function() { - directive('replaceWithComments', valueFn({ - replace: true, - templateUrl: 'templateWithComments.html' - })); - }); - inject(function($compile, $rootScope, $httpBackend) { - $httpBackend.whenGET('templateWithComments.html'). - respond('

Hello, world!

'); - expect(function() { - element = $compile('
')($rootScope); - }).not.toThrow(); - $httpBackend.flush(); - expect(element.find('p').length).toBe(1); - expect(element.find('p').text()).toBe('Hello, world!'); - }); - }); - - it('should ignore whitespace between comment and root node when replacing with a templateUrl', function() { - module(function() { - directive('replaceWithWhitespace', valueFn({ - replace: true, - templateUrl: 'templateWithWhitespace.html' - })); - }); - inject(function($compile, $rootScope, $httpBackend) { - $httpBackend.whenGET('templateWithWhitespace.html'). - respond('

Hello, world!

'); - expect(function() { - element = $compile('
')($rootScope); - }).not.toThrow(); - $httpBackend.flush(); - expect(element.find('p').length).toBe(1); - expect(element.find('p').text()).toBe('Hello, world!'); - }); - }); - - it('should keep prototype properties on sync version of async directive', function() { module(function() { function DirectiveClass() { From 406c1b094bc0a8a79b06f0d5f0cecdfb9f6087b0 Mon Sep 17 00:00:00 2001 From: Georgii Dolzhykov Date: Thu, 13 Oct 2016 15:23:18 +0300 Subject: [PATCH 0072/1014] docs($rootScope.Scope): grammar Closes #15263 --- src/ng/rootScope.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 7731c646dfee..618c7b93872c 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -436,8 +436,8 @@ function $RootScopeProvider() { * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. * If any one expression in the collection changes the `listener` is executed. * - * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every - * call to $digest() to see if any items changes. + * - The items in the `watchExpressions` array are observed via the standard `$watch` operation. Their return + * values are examined for changes on every call to `$digest`. * - The `listener` is called whenever any expression in the `watchExpressions` array changes. * * @param {Array.} watchExpressions Array of expressions that will be individually From b8c8262808f2e1527f5e7607b40765efee71c975 Mon Sep 17 00:00:00 2001 From: Venkat Ganesan Date: Sun, 16 Oct 2016 20:34:22 -0700 Subject: [PATCH 0073/1014] docs(input[checkbox]): mention `ngChecked` Closes #14465 Closes #15277 --- src/ng/directive/input.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 7e26ca16f6d1..47a0fa8db508 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1098,6 +1098,9 @@ var inputType = { * Can be interpolated. * @param {string=} ngChange Angular expression to be executed when the ngModel value changes due * to user interaction with the input element. + * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the + * element. **Note** : `ngChecked` should not be used alongside `ngModel`. + * Checkout {@link ng.directive:ngChecked ngChecked} for usage. * * @example From aa6a80618d1e191501488f50b4e21b43f63eabdf Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Sun, 16 Oct 2016 14:14:16 +0300 Subject: [PATCH 0074/1014] chore(tutorial): make diagram images responsive Fixes angular/angular-phonecat#376 Closes #15275 --- docs/app/assets/css/docs.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/app/assets/css/docs.css b/docs/app/assets/css/docs.css index eb4fa813086e..c9b51256876e 100644 --- a/docs/app/assets/css/docs.css +++ b/docs/app/assets/css/docs.css @@ -650,6 +650,7 @@ ul.events > li { .diagram { margin-bottom: 10px; margin-top: 30px; + max-width: 100%; } @media only screen and (min-width: 769px) and (max-width: 991px) { From 705afcd160c8428133b36f2cd63db305dc52f2d7 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 17 Oct 2016 12:21:29 +0200 Subject: [PATCH 0075/1014] fix($location): prevent infinite digest with IDN urls in Edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Internationalized Domain Urls, for example urls with Umlaut (Ä, Ö, Ü) cause infinite digest in Edge 38.14393.0.0 because lastIndexOf doesn't work correctly in this version when the search string is the same as the haystack string. The patch uses an implementation based on core.js: https://github.com/zloirock/core-js/blob/v2.4.1/modules/es6.string.starts-with.js#L16 Edge Bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9271625/ Fixes #15217 PR #15235 --- src/ng/location.js | 4 ++-- test/ng/locationSpec.js | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ng/location.js b/src/ng/location.js index 9de7702d0194..53a20cda62f0 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -48,8 +48,8 @@ function parseAppUrl(relativeUrl, locationObj) { } } -function startsWith(haystack, needle) { - return haystack.lastIndexOf(needle, 0) === 0; +function startsWith(str, search) { + return str.slice(0, search.length) === search; } /** diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index 2692d2cccc35..21e171198f7b 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -2450,10 +2450,11 @@ describe('$location', function() { describe('LocationHtml5Url', function() { - var locationUrl, locationIndexUrl; + var locationUrl, locationUmlautUrl, locationIndexUrl; beforeEach(function() { locationUrl = new LocationHtml5Url('http://server/pre/', 'http://server/pre/', 'http://server/pre/path'); + locationUmlautUrl = new LocationHtml5Url('http://särver/pre/', 'http://särver/pre/', 'http://särver/pre/path'); locationIndexUrl = new LocationHtml5Url('http://server/pre/index.html', 'http://server/pre/', 'http://server/pre/path'); }); @@ -2465,6 +2466,13 @@ describe('$location', function() { // Note: relies on the previous state! expect(parseLinkAndReturn(locationUrl, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/otherPath#test'); + expect(parseLinkAndReturn(locationUmlautUrl, 'http://other')).toEqual(undefined); + expect(parseLinkAndReturn(locationUmlautUrl, 'http://särver/pre')).toEqual('http://särver/pre/'); + expect(parseLinkAndReturn(locationUmlautUrl, 'http://särver/pre/')).toEqual('http://särver/pre/'); + expect(parseLinkAndReturn(locationUmlautUrl, 'http://särver/pre/otherPath')).toEqual('http://särver/pre/otherPath'); + // Note: relies on the previous state! + expect(parseLinkAndReturn(locationUmlautUrl, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://särver/pre/otherPath#test'); + expect(parseLinkAndReturn(locationIndexUrl, 'http://server/pre')).toEqual('http://server/pre/'); expect(parseLinkAndReturn(locationIndexUrl, 'http://server/pre/')).toEqual('http://server/pre/'); expect(parseLinkAndReturn(locationIndexUrl, 'http://server/pre/otherPath')).toEqual('http://server/pre/otherPath'); From db02008fe274410d2b8e2715fa4a3c8c9b2ce809 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 17 Oct 2016 19:31:39 +0200 Subject: [PATCH 0076/1014] chore(doc-gen, docs-app): create plnkr examples with correct Angular version When the docs are based on the snapshot, the plnkr examples must use the snapshot files from code.angularjs.org Closes #15267 PR (#15269) --- docs/app/src/examples.js | 5 +-- docs/config/index.js | 7 +++- docs/config/services/deployments/plnkr.js | 49 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 docs/config/services/deployments/plnkr.js diff --git a/docs/app/src/examples.js b/docs/app/src/examples.js index fca74d9fc21a..a5434f0364f7 100644 --- a/docs/app/src/examples.js +++ b/docs/app/src/examples.js @@ -181,9 +181,8 @@ angular.module('examples', []) filePromises.push($http.get(exampleFolder + '/' + filename, { transformResponse: [] }) .then(function(response) { - // The manifests provide the production index file but Plunkr wants - // a straight index.html - if (filename === 'index-production.html') { + // Plunkr needs an index file that's simply named index.html + if (filename === 'index-plnkr.html') { filename = 'index.html'; } diff --git a/docs/config/index.js b/docs/config/index.js index 4ec1423c65a0..c4662d3197eb 100644 --- a/docs/config/index.js +++ b/docs/config/index.js @@ -23,6 +23,7 @@ module.exports = new Package('angularjs', [ .factory(require('./services/deployments/default')) .factory(require('./services/deployments/jquery')) .factory(require('./services/deployments/production')) +.factory(require('./services/deployments/plnkr')) .factory(require('./inline-tag-defs/type')) @@ -150,7 +151,8 @@ module.exports = new Package('angularjs', [ generateProtractorTestsProcessor, generateExamplesProcessor, debugDeployment, defaultDeployment, - jqueryDeployment, productionDeployment) { + jqueryDeployment, productionDeployment, + plnkrDeployment) { generateIndexPagesProcessor.deployments = [ debugDeployment, @@ -170,7 +172,8 @@ module.exports = new Package('angularjs', [ debugDeployment, defaultDeployment, jqueryDeployment, - productionDeployment + productionDeployment, + plnkrDeployment ]; }) diff --git a/docs/config/services/deployments/plnkr.js b/docs/config/services/deployments/plnkr.js new file mode 100644 index 000000000000..f6967bc5abd8 --- /dev/null +++ b/docs/config/services/deployments/plnkr.js @@ -0,0 +1,49 @@ +'use strict'; +// Special deployment that is only used for the examples on plnkr. +// While the embedded examples use the Angular files relative the docs folder, +// plnkr uses the CDN urls, and needs to switch between Google CDN for tagged versions +// and the code.angularjs.org server for the snapshot (master) version. + +var versionInfo = require('../../../../lib/versions/version-info'); +var isSnapshot = versionInfo.currentVersion.isSnapshot; + +var cdnUrl = isSnapshot ? + '//code.angularjs.org/snapshot' : + '//ajax.googleapis.com/ajax/libs/angularjs/' + versionInfo.cdnVersion; + +module.exports = function plnkrDeployment(getVersion) { + return { + name: 'plnkr', + examples: { + commonFiles: { + scripts: [cdnUrl + '/angular.min.js'] + }, + dependencyPath: cdnUrl + '/' + }, + scripts: [ + cdnUrl + '/angular.min.js', + cdnUrl + '/angular-resource.min.js', + cdnUrl + '/angular-route.min.js', + cdnUrl + '/angular-cookies.min.js', + cdnUrl + '/angular-sanitize.min.js', + cdnUrl + '/angular-touch.min.js', + cdnUrl + '/angular-animate.min.js', + 'components/marked-' + getVersion('marked', 'node_modules', 'package.json') + '/lib/marked.js', + 'js/angular-bootstrap/dropdown-toggle.min.js', + 'components/lunr.js-' + getVersion('lunr.js') + '/lunr.min.js', + 'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js', + 'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/lang-css.js', + 'js/versions-data.js', + 'js/pages-data.js', + 'js/nav-data.js', + 'js/docs.min.js' + ], + stylesheets: [ + 'components/bootstrap-' + getVersion('bootstrap') + '/css/bootstrap.min.css', + 'components/open-sans-fontface-' + getVersion('open-sans-fontface') + '/open-sans.css', + 'css/prettify-theme.css', + 'css/docs.css', + 'css/animations.css' + ] + }; +}; From 19973609f43af781b1c4467f3acbc42b3a8f791e Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 17 Oct 2016 19:32:15 +0200 Subject: [PATCH 0077/1014] chore(docs-app): show loader when loading view / partial Closes #14385 PR (#15280) --- docs/app/src/docs.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/app/src/docs.js b/docs/app/src/docs.js index 3593009b653b..287e5bfd7200 100644 --- a/docs/app/src/docs.js +++ b/docs/app/src/docs.js @@ -23,6 +23,11 @@ angular.module('DocsController', []) $scope.$on('$includeContentLoaded', function() { var pagePath = $scope.currentPage ? $scope.currentPage.path : $location.path(); $window._gaq.push(['_trackPageview', pagePath]); + $scope.loading = false; + }); + + $scope.$on('$includeContentError', function() { + $scope.loading = false; }); $scope.$watch(function docsPathWatch() {return $location.path(); }, function docsPathWatchAction(path) { @@ -31,6 +36,8 @@ angular.module('DocsController', []) var currentPage = $scope.currentPage = NG_PAGES[path]; + $scope.loading = true; + if (currentPage) { $scope.partialPath = 'partials/' + path + '.html'; $scope.currentArea = NG_NAVIGATION[currentPage.area]; From daa47e33e38d1c097a94494f19e4e492d049f608 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 17 Oct 2016 23:16:41 +0200 Subject: [PATCH 0078/1014] Revert "chore(doc-gen, docs-app): create plnkr examples with correct Angular version" This patch relies on a change in the dgeni example package, which has not been added to dgeni yet. This reverts commit db02008fe274410d2b8e2715fa4a3c8c9b2ce809. --- docs/app/src/examples.js | 5 ++- docs/config/index.js | 7 +--- docs/config/services/deployments/plnkr.js | 49 ----------------------- 3 files changed, 5 insertions(+), 56 deletions(-) delete mode 100644 docs/config/services/deployments/plnkr.js diff --git a/docs/app/src/examples.js b/docs/app/src/examples.js index a5434f0364f7..fca74d9fc21a 100644 --- a/docs/app/src/examples.js +++ b/docs/app/src/examples.js @@ -181,8 +181,9 @@ angular.module('examples', []) filePromises.push($http.get(exampleFolder + '/' + filename, { transformResponse: [] }) .then(function(response) { - // Plunkr needs an index file that's simply named index.html - if (filename === 'index-plnkr.html') { + // The manifests provide the production index file but Plunkr wants + // a straight index.html + if (filename === 'index-production.html') { filename = 'index.html'; } diff --git a/docs/config/index.js b/docs/config/index.js index c4662d3197eb..4ec1423c65a0 100644 --- a/docs/config/index.js +++ b/docs/config/index.js @@ -23,7 +23,6 @@ module.exports = new Package('angularjs', [ .factory(require('./services/deployments/default')) .factory(require('./services/deployments/jquery')) .factory(require('./services/deployments/production')) -.factory(require('./services/deployments/plnkr')) .factory(require('./inline-tag-defs/type')) @@ -151,8 +150,7 @@ module.exports = new Package('angularjs', [ generateProtractorTestsProcessor, generateExamplesProcessor, debugDeployment, defaultDeployment, - jqueryDeployment, productionDeployment, - plnkrDeployment) { + jqueryDeployment, productionDeployment) { generateIndexPagesProcessor.deployments = [ debugDeployment, @@ -172,8 +170,7 @@ module.exports = new Package('angularjs', [ debugDeployment, defaultDeployment, jqueryDeployment, - productionDeployment, - plnkrDeployment + productionDeployment ]; }) diff --git a/docs/config/services/deployments/plnkr.js b/docs/config/services/deployments/plnkr.js deleted file mode 100644 index f6967bc5abd8..000000000000 --- a/docs/config/services/deployments/plnkr.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; -// Special deployment that is only used for the examples on plnkr. -// While the embedded examples use the Angular files relative the docs folder, -// plnkr uses the CDN urls, and needs to switch between Google CDN for tagged versions -// and the code.angularjs.org server for the snapshot (master) version. - -var versionInfo = require('../../../../lib/versions/version-info'); -var isSnapshot = versionInfo.currentVersion.isSnapshot; - -var cdnUrl = isSnapshot ? - '//code.angularjs.org/snapshot' : - '//ajax.googleapis.com/ajax/libs/angularjs/' + versionInfo.cdnVersion; - -module.exports = function plnkrDeployment(getVersion) { - return { - name: 'plnkr', - examples: { - commonFiles: { - scripts: [cdnUrl + '/angular.min.js'] - }, - dependencyPath: cdnUrl + '/' - }, - scripts: [ - cdnUrl + '/angular.min.js', - cdnUrl + '/angular-resource.min.js', - cdnUrl + '/angular-route.min.js', - cdnUrl + '/angular-cookies.min.js', - cdnUrl + '/angular-sanitize.min.js', - cdnUrl + '/angular-touch.min.js', - cdnUrl + '/angular-animate.min.js', - 'components/marked-' + getVersion('marked', 'node_modules', 'package.json') + '/lib/marked.js', - 'js/angular-bootstrap/dropdown-toggle.min.js', - 'components/lunr.js-' + getVersion('lunr.js') + '/lunr.min.js', - 'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js', - 'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/lang-css.js', - 'js/versions-data.js', - 'js/pages-data.js', - 'js/nav-data.js', - 'js/docs.min.js' - ], - stylesheets: [ - 'components/bootstrap-' + getVersion('bootstrap') + '/css/bootstrap.min.css', - 'components/open-sans-fontface-' + getVersion('open-sans-fontface') + '/open-sans.css', - 'css/prettify-theme.css', - 'css/docs.css', - 'css/animations.css' - ] - }; -}; From 18263f1c527f656c0b7194d128b2de7ed2d90641 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 19 Oct 2016 11:55:50 +0200 Subject: [PATCH 0079/1014] chore(docs-app): improve layout when loading partials By setting the current partial content to hidden, the current height of the window is maintained until the new content is loaded. This prevents flickering caused by the scrollbar (dis)appearing and the footer coming into view. --- docs/app/assets/css/docs.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/app/assets/css/docs.css b/docs/app/assets/css/docs.css index c9b51256876e..c095047aaad1 100644 --- a/docs/app/assets/css/docs.css +++ b/docs/app/assets/css/docs.css @@ -653,6 +653,13 @@ ul.events > li { max-width: 100%; } +@media only screen and (min-width: 769px) { + [ng-include="partialPath"].ng-hide { + display: block !important; + visibility: hidden; + } +} + @media only screen and (min-width: 769px) and (max-width: 991px) { .main-body-grid { margin-top: 160px; From 00b60f2f03c46f4e8ee40b518747f15f33bbfebc Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 19 Oct 2016 12:12:56 +0200 Subject: [PATCH 0080/1014] docs(a): remove outdated practice Using a tags as buttons is bad for accessibility and usability --- src/ng/directive/a.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ng/directive/a.js b/src/ng/directive/a.js index e1e57195e8c6..17913a043b72 100644 --- a/src/ng/directive/a.js +++ b/src/ng/directive/a.js @@ -6,12 +6,10 @@ * @restrict E * * @description - * Modifies the default behavior of the html A tag so that the default action is prevented when + * Modifies the default behavior of the html a tag so that the default action is prevented when * the href attribute is empty. * - * This change permits the easy creation of action links with the `ngClick` directive - * without changing the location or causing page reloads, e.g.: - * `Add Item` + * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive. */ var htmlAnchorDirective = valueFn({ restrict: 'E', From d6c91ea17589a6401e766dac70367bc48d7d3b74 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 13 Oct 2016 19:57:12 +0300 Subject: [PATCH 0081/1014] refactor(input): avoid duplicating `step`/`ngStep` tests --- test/ng/directive/inputSpec.js | 208 +++++++++++---------------------- 1 file changed, 70 insertions(+), 138 deletions(-) diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 15471354c56f..9973c2411cd3 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -2621,154 +2621,88 @@ describe('input', function() { }); }); - describe('step', function() { - it('should validate', function() { - $rootScope.step = 10; - $rootScope.value = 20; - var inputElm = helper.compileInput(''); - - expect(inputElm.val()).toBe('20'); - expect(inputElm).toBeValid(); - expect($rootScope.value).toBe(20); - expect($rootScope.form.alias.$error.step).toBeFalsy(); - - helper.changeInputValueTo('18'); - expect(inputElm).toBeInvalid(); - expect(inputElm.val()).toBe('18'); - expect($rootScope.value).toBeUndefined(); - expect($rootScope.form.alias.$error.step).toBeTruthy(); - - helper.changeInputValueTo('10'); - expect(inputElm).toBeValid(); - expect(inputElm.val()).toBe('10'); - expect($rootScope.value).toBe(10); - expect($rootScope.form.alias.$error.step).toBeFalsy(); - - $rootScope.$apply('value = 12'); - expect(inputElm).toBeInvalid(); - expect(inputElm.val()).toBe('12'); - expect($rootScope.value).toBe(12); - expect($rootScope.form.alias.$error.step).toBeTruthy(); - }); - - it('should validate even if the step value changes on-the-fly', function() { - $rootScope.step = 10; - var inputElm = helper.compileInput(''); - - helper.changeInputValueTo('10'); - expect(inputElm).toBeValid(); - expect($rootScope.value).toBe(10); - - // Step changes, but value matches - $rootScope.$apply('step = 5'); - expect(inputElm.val()).toBe('10'); - expect(inputElm).toBeValid(); - expect($rootScope.value).toBe(10); - expect($rootScope.form.alias.$error.step).toBeFalsy(); - - // Step changes, value does not match - $rootScope.$apply('step = 6'); - expect(inputElm).toBeInvalid(); - expect($rootScope.value).toBeUndefined(); - expect(inputElm.val()).toBe('10'); - expect($rootScope.form.alias.$error.step).toBeTruthy(); - - // null = valid - $rootScope.$apply('step = null'); - expect(inputElm).toBeValid(); - expect($rootScope.value).toBe(10); - expect(inputElm.val()).toBe('10'); - expect($rootScope.form.alias.$error.step).toBeFalsy(); - - // Step val as string - $rootScope.$apply('step = "7"'); - expect(inputElm).toBeInvalid(); - expect($rootScope.value).toBeUndefined(); - expect(inputElm.val()).toBe('10'); - expect($rootScope.form.alias.$error.step).toBeTruthy(); - // unparsable string is ignored - $rootScope.$apply('step = "abc"'); - expect(inputElm).toBeValid(); - expect($rootScope.value).toBe(10); - expect(inputElm.val()).toBe('10'); - expect($rootScope.form.alias.$error.step).toBeFalsy(); - }); - }); + forEach({ + step: 'step="{{step}}"', + ngStep: 'ng-step="step"' + }, function(attrHtml, attrName) { + describe(attrName, function() { - describe('ngStep', function() { - it('should validate', function() { - $rootScope.step = 10; - $rootScope.value = 20; - var inputElm = helper.compileInput(''); + it('should validate', function() { + $rootScope.step = 10; + $rootScope.value = 20; + var inputElm = helper.compileInput( + ''); - expect(inputElm.val()).toBe('20'); - expect(inputElm).toBeValid(); - expect($rootScope.value).toBe(20); - expect($rootScope.form.alias.$error.step).toBeFalsy(); + expect(inputElm.val()).toBe('20'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(20); + expect($rootScope.form.alias.$error.step).toBeFalsy(); - helper.changeInputValueTo('18'); - expect(inputElm).toBeInvalid(); - expect(inputElm.val()).toBe('18'); - expect($rootScope.value).toBeUndefined(); - expect($rootScope.form.alias.$error.step).toBeTruthy(); + helper.changeInputValueTo('18'); + expect(inputElm).toBeInvalid(); + expect(inputElm.val()).toBe('18'); + expect($rootScope.value).toBeUndefined(); + expect($rootScope.form.alias.$error.step).toBeTruthy(); - helper.changeInputValueTo('10'); - expect(inputElm).toBeValid(); - expect(inputElm.val()).toBe('10'); - expect($rootScope.value).toBe(10); - expect($rootScope.form.alias.$error.step).toBeFalsy(); + helper.changeInputValueTo('10'); + expect(inputElm).toBeValid(); + expect(inputElm.val()).toBe('10'); + expect($rootScope.value).toBe(10); + expect($rootScope.form.alias.$error.step).toBeFalsy(); - $rootScope.$apply('value = 12'); - expect(inputElm).toBeInvalid(); - expect(inputElm.val()).toBe('12'); - expect($rootScope.value).toBe(12); - expect($rootScope.form.alias.$error.step).toBeTruthy(); - }); + $rootScope.$apply('value = 12'); + expect(inputElm).toBeInvalid(); + expect(inputElm.val()).toBe('12'); + expect($rootScope.value).toBe(12); + expect($rootScope.form.alias.$error.step).toBeTruthy(); + }); - it('should validate even if the step value changes on-the-fly', function() { - $rootScope.step = 10; - var inputElm = helper.compileInput(''); + it('should validate even if the step value changes on-the-fly', function() { + $rootScope.step = 10; + var inputElm = helper.compileInput( + ''); - helper.changeInputValueTo('10'); - expect(inputElm).toBeValid(); - expect($rootScope.value).toBe(10); + helper.changeInputValueTo('10'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(10); - // Step changes, but value matches - $rootScope.$apply('step = 5'); - expect(inputElm.val()).toBe('10'); - expect(inputElm).toBeValid(); - expect($rootScope.value).toBe(10); - expect($rootScope.form.alias.$error.step).toBeFalsy(); + // Step changes, but value matches + $rootScope.$apply('step = 5'); + expect(inputElm.val()).toBe('10'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(10); + expect($rootScope.form.alias.$error.step).toBeFalsy(); - // Step changes, value does not match - $rootScope.$apply('step = 6'); - expect(inputElm).toBeInvalid(); - expect($rootScope.value).toBeUndefined(); - expect(inputElm.val()).toBe('10'); - expect($rootScope.form.alias.$error.step).toBeTruthy(); + // Step changes, value does not match + $rootScope.$apply('step = 6'); + expect(inputElm).toBeInvalid(); + expect($rootScope.value).toBeUndefined(); + expect(inputElm.val()).toBe('10'); + expect($rootScope.form.alias.$error.step).toBeTruthy(); - // null = valid - $rootScope.$apply('step = null'); - expect(inputElm).toBeValid(); - expect($rootScope.value).toBe(10); - expect(inputElm.val()).toBe('10'); - expect($rootScope.form.alias.$error.step).toBeFalsy(); + // null = valid + $rootScope.$apply('step = null'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(10); + expect(inputElm.val()).toBe('10'); + expect($rootScope.form.alias.$error.step).toBeFalsy(); - // Step val as string - $rootScope.$apply('step = "7"'); - expect(inputElm).toBeInvalid(); - expect($rootScope.value).toBeUndefined(); - expect(inputElm.val()).toBe('10'); - expect($rootScope.form.alias.$error.step).toBeTruthy(); + // Step val as string + $rootScope.$apply('step = "7"'); + expect(inputElm).toBeInvalid(); + expect($rootScope.value).toBeUndefined(); + expect(inputElm.val()).toBe('10'); + expect($rootScope.form.alias.$error.step).toBeTruthy(); - // unparsable string is ignored - $rootScope.$apply('step = "abc"'); - expect(inputElm).toBeValid(); - expect($rootScope.value).toBe(10); - expect(inputElm.val()).toBe('10'); - expect($rootScope.form.alias.$error.step).toBeFalsy(); + // unparsable string is ignored + $rootScope.$apply('step = "abc"'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(10); + expect(inputElm.val()).toBe('10'); + expect($rootScope.form.alias.$error.step).toBeFalsy(); + }); }); }); @@ -3001,7 +2935,6 @@ describe('input', function() { }); describe('range', function() { - var scope; var rangeTestEl = angular.element(''); @@ -3048,7 +2981,6 @@ describe('input', function() { expect(scope.age).toBe(50); expect(inputElm).toBeValid(); }); - } else { it('should reset the model if view is invalid', function() { @@ -3438,7 +3370,6 @@ describe('input', function() { expect(scope.value).toBe(40); }); }); - } @@ -3448,6 +3379,7 @@ describe('input', function() { // Browsers that implement range will never allow you to set a value that doesn't match the step value // However, currently only Firefox fully implements the spec when setting the value after the step value changes. // Other browsers fail in various edge cases, which is why they are not tested here. + it('should round the input value to the nearest step on user input', function() { var inputElm = helper.compileInput(''); @@ -3510,8 +3442,8 @@ describe('input', function() { expect(scope.value).toBe(10); expect(scope.form.alias.$error.step).toBeFalsy(); }); - } else { + it('should validate if "range" is not implemented', function() { scope.step = 10; scope.value = 20; From 081d06ffd15c2c6c539ce97b5eb63fa8e2403818 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 13 Oct 2016 19:59:23 +0300 Subject: [PATCH 0082/1014] fix(input): fix `step` validation for `input[number]`/`input[range]` Related to 9a8b8aa and #15257. Fixes the issue discussed in https://github.com/angular/angular.js/commit/9a8b8aa#commitcomment-19108436. Fixes #15257 Closes #15264 --- src/ng/directive/input.js | 58 +++++++++++- test/ng/directive/inputSpec.js | 166 +++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 4 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 47a0fa8db508..8d8afcdb8306 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1532,13 +1532,62 @@ function parseNumberAttrVal(val) { return !isNumberNaN(val) ? val : undefined; } +function isNumberInteger(num) { + // See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066 + // (minus the assumption that `num` is a number) + + // eslint-disable-next-line no-bitwise + return (num | 0) === num; +} + +function countDecimals(num) { + var numString = num.toString(); + var decimalSymbolIndex = numString.indexOf('.'); + + if (decimalSymbolIndex === -1) { + if (-1 < num && num < 1) { + // It may be in the exponential notation format (`1e-X`) + var match = /e-(\d+)$/.exec(numString); + + if (match) { + return Number(match[1]); + } + } + + return 0; + } + + return numString.length - decimalSymbolIndex - 1; +} + +function isValidForStep(viewValue, stepBase, step) { + // At this point `stepBase` and `step` are expected to be non-NaN values + // and `viewValue` is expected to be a valid stringified number. + var value = Number(viewValue); + + // Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or + // `0.5 % 0.1 !== 0`), we need to convert all numbers to integers. + if (!isNumberInteger(value) || !isNumberInteger(stepBase) || !isNumberInteger(step)) { + var decimalCount = Math.max(countDecimals(value), countDecimals(stepBase), countDecimals(step)); + var multiplier = Math.pow(10, decimalCount); + + value = value * multiplier; + stepBase = stepBase * multiplier; + step = step * multiplier; + } + + return (value - stepBase) % step === 0; +} + function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { badInputChecker(scope, element, attr, ctrl); numberFormatterParser(ctrl); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + var minVal; + var maxVal; + if (isDefined(attr.min) || attr.ngMin) { - var minVal; ctrl.$validators.min = function(value) { return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; }; @@ -1551,7 +1600,6 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { } if (isDefined(attr.max) || attr.ngMax) { - var maxVal; ctrl.$validators.max = function(value) { return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; }; @@ -1566,7 +1614,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { if (isDefined(attr.step) || attr.ngStep) { var stepVal; ctrl.$validators.step = function(modelValue, viewValue) { - return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0; + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); }; attr.$observe('step', function(val) { @@ -1636,7 +1685,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { } : // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would function stepValidator(modelValue, viewValue) { - return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || viewValue % stepVal === 0; + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); }; setInitialValueAndObserver('step', stepChange); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 9973c2411cd3..40ceff4ac879 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -2703,6 +2703,91 @@ describe('input', function() { expect(inputElm.val()).toBe('10'); expect($rootScope.form.alias.$error.step).toBeFalsy(); }); + + it('should use the correct "step base" when `[min]` is specified', function() { + $rootScope.min = 5; + $rootScope.step = 10; + $rootScope.value = 10; + var inputElm = helper.compileInput( + ''); + var ngModel = inputElm.controller('ngModel'); + + expect(inputElm.val()).toBe('10'); + expect(inputElm).toBeInvalid(); + expect(ngModel.$error.step).toBe(true); + expect($rootScope.value).toBeUndefined(); + + helper.changeInputValueTo('15'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(15); + + $rootScope.$apply('step = 3'); + expect(inputElm.val()).toBe('15'); + expect(inputElm).toBeInvalid(); + expect(ngModel.$error.step).toBe(true); + expect($rootScope.value).toBeUndefined(); + + helper.changeInputValueTo('8'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(8); + + $rootScope.$apply('min = 10; step = 20'); + helper.changeInputValueTo('30'); + expect(inputElm.val()).toBe('30'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(30); + + $rootScope.$apply('min = 5'); + expect(inputElm.val()).toBe('30'); + expect(inputElm).toBeInvalid(); + expect(ngModel.$error.step).toBe(true); + expect($rootScope.value).toBeUndefined(); + + $rootScope.$apply('step = 0.00000001'); + expect(inputElm.val()).toBe('30'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(30); + + // 0.3 - 0.2 === 0.09999999999999998 + $rootScope.$apply('min = 0.2; step = (0.3 - 0.2)'); + helper.changeInputValueTo('0.3'); + expect(inputElm.val()).toBe('0.3'); + expect(inputElm).toBeInvalid(); + expect(ngModel.$error.step).toBe(true); + expect($rootScope.value).toBeUndefined(); + }); + + it('should correctly validate even in cases where the JS floating point arithmetic fails', + function() { + $rootScope.step = 0.1; + var inputElm = helper.compileInput( + ''); + var ngModel = inputElm.controller('ngModel'); + + expect(inputElm.val()).toBe(''); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBeUndefined(); + + helper.changeInputValueTo('0.3'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(0.3); + + helper.changeInputValueTo('2.9999999999999996'); + expect(inputElm).toBeInvalid(); + expect(ngModel.$error.step).toBe(true); + expect($rootScope.value).toBeUndefined(); + + // 0.5 % 0.1 === 0.09999999999999998 + helper.changeInputValueTo('0.5'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(0.5); + + // 3.5 % 0.1 === 0.09999999999999981 + helper.changeInputValueTo('3.5'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(3.5); + } + ); }); }); @@ -3516,6 +3601,87 @@ describe('input', function() { expect(inputElm.val()).toBe('10'); expect(scope.form.alias.$error.step).toBeFalsy(); }); + + it('should use the correct "step base" when `[min]` is specified', function() { + $rootScope.min = 5; + $rootScope.step = 10; + $rootScope.value = 10; + var inputElm = helper.compileInput( + ''); + var ngModel = inputElm.controller('ngModel'); + + expect(inputElm.val()).toBe('10'); + expect(inputElm).toBeInvalid(); + expect(ngModel.$error.step).toBe(true); + expect($rootScope.value).toBeUndefined(); + + helper.changeInputValueTo('15'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(15); + + $rootScope.$apply('step = 3'); + expect(inputElm.val()).toBe('15'); + expect(inputElm).toBeInvalid(); + expect(ngModel.$error.step).toBe(true); + expect($rootScope.value).toBeUndefined(); + + helper.changeInputValueTo('8'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(8); + + $rootScope.$apply('min = 10; step = 20; value = 30'); + expect(inputElm.val()).toBe('30'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(30); + + $rootScope.$apply('min = 5'); + expect(inputElm.val()).toBe('30'); + expect(inputElm).toBeInvalid(); + expect(ngModel.$error.step).toBe(true); + expect($rootScope.value).toBeUndefined(); + + $rootScope.$apply('step = 0.00000001'); + expect(inputElm.val()).toBe('30'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(30); + + // 0.3 - 0.2 === 0.09999999999999998 + $rootScope.$apply('min = 0.2; step = 0.09999999999999998; value = 0.3'); + expect(inputElm.val()).toBe('0.3'); + expect(inputElm).toBeInvalid(); + expect(ngModel.$error.step).toBe(true); + expect($rootScope.value).toBeUndefined(); + }); + + it('should correctly validate even in cases where the JS floating point arithmetic fails', + function() { + var inputElm = helper.compileInput(''); + var ngModel = inputElm.controller('ngModel'); + + expect(inputElm.val()).toBe(''); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBeUndefined(); + + helper.changeInputValueTo('0.3'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(0.3); + + helper.changeInputValueTo('2.9999999999999996'); + expect(inputElm).toBeInvalid(); + expect(ngModel.$error.step).toBe(true); + expect($rootScope.value).toBeUndefined(); + + // 0.5 % 0.1 === 0.09999999999999998 + helper.changeInputValueTo('0.5'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(0.5); + + // 3.5 % 0.1 === 0.09999999999999981 + helper.changeInputValueTo('3.5'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(3.5); + } + ); } }); }); From 7dacbcc991657dacbec7b0cad4df318a8075ec04 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Wed, 19 Oct 2016 15:27:38 +0300 Subject: [PATCH 0083/1014] test(input): fix typo (`step="{{step}}""` --> `step="{{step}}"`) --- test/ng/directive/inputSpec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 40ceff4ac879..d7063f2c6c7d 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -3607,7 +3607,7 @@ describe('input', function() { $rootScope.step = 10; $rootScope.value = 10; var inputElm = helper.compileInput( - ''); + ''); var ngModel = inputElm.controller('ngModel'); expect(inputElm.val()).toBe('10'); From 35482babd9eb0970edbc99f223e04594a9d09364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski?= Date: Mon, 17 Oct 2016 23:08:41 +0200 Subject: [PATCH 0084/1014] refactor($sniffer): remove $sniffer.vendorPrefix Previously, Angular tried to detect the CSS prefix the browser supports and then use the saved one. This strategy is not ideal as currently some browsers are supporting more than one vendor prefix. The best example is Microsoft Edge that added -webkit- prefixes to be more Web-compatible; Firefox is doing a similar thing on mobile. Some of the -webkit--prefixed things are now even getting into specs to sanction that they're now required for Web compatibility. In some cases Edge even supports only the -webkit--prefixed property; one example is -webkit-appearance. $sniffer.vendorPrefix is no longer used in Angular core outside of $sniffer itself; taking that and the above problems into account, it's better to just remove it. The only remaining use case was an internal use in detection of support for transitions/animations but we can directly check the webkit prefix there manually; no other prefix matters for them anyway. $sniffer is undocumented API so this removal is not a breaking change. However, if you've previously been using it in your code, just paste the following to get the same function: var vendorPrefix = (function() { var prefix, prop, match; var vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/; for (prop in document.createElement('div').style) { if ((match = vendorRegex.exec(prop))) { prefix = match[0]; break; } } return prefix; })(); The vendorPrefix variable will contain what $sniffer.vendorPrefix used to. Note that we advise to not check for vendor prefixes this way; if you have to do it, it's better to check it separately for each CSS property used for the reasons described at the beginning. If you use jQuery, you don't have to do anything; it automatically adds vendor prefixes to CSS prefixes for you in the .css() method. Fixes #13690 Closes #15287 --- src/ng/sniffer.js | 29 ++++----------------- test/helpers/privateMocks.js | 16 +++++++----- test/ng/snifferSpec.js | 43 +++----------------------------- test/ngAnimate/animateCssSpec.js | 15 ++++++----- 4 files changed, 26 insertions(+), 77 deletions(-) diff --git a/src/ng/sniffer.js b/src/ng/sniffer.js index 60ed39d81318..217f3ba9d0a7 100644 --- a/src/ng/sniffer.js +++ b/src/ng/sniffer.js @@ -34,33 +34,15 @@ function $SnifferProvider() { toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), document = $document[0] || {}, - vendorPrefix, - vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/, bodyStyle = document.body && document.body.style, transitions = false, - animations = false, - match; + animations = false; if (bodyStyle) { - for (var prop in bodyStyle) { - if ((match = vendorRegex.exec(prop))) { - vendorPrefix = match[0]; - vendorPrefix = vendorPrefix[0].toUpperCase() + vendorPrefix.substr(1); - break; - } - } - - if (!vendorPrefix) { - vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; - } - - transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); - animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); - - if (android && (!transitions || !animations)) { - transitions = isString(bodyStyle.webkitTransition); - animations = isString(bodyStyle.webkitAnimation); - } + // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x + // Mentioned browsers need a -webkit- prefix for transitions & animations. + transitions = !!('transition' in bodyStyle || 'webkitTransition' in bodyStyle); + animations = !!('animation' in bodyStyle || 'webkitAnimation' in bodyStyle); } @@ -90,7 +72,6 @@ function $SnifferProvider() { return eventSupport[event]; }, csp: csp(), - vendorPrefix: vendorPrefix, transitions: transitions, animations: animations, android: android diff --git a/test/helpers/privateMocks.js b/test/helpers/privateMocks.js index 1a1580b2c118..897e2a290e5d 100644 --- a/test/helpers/privateMocks.js +++ b/test/helpers/privateMocks.js @@ -36,7 +36,7 @@ function browserSupportsCssAnimations() { return !(window.document.documentMode < 10); } -function createMockStyleSheet(doc, prefix) { +function createMockStyleSheet(doc) { doc = doc ? doc[0] : window.document; var node = doc.createElement('style'); @@ -57,13 +57,17 @@ function createMockStyleSheet(doc, prefix) { }, addPossiblyPrefixedRule: function(selector, styles) { - if (prefix) { - var prefixedStyles = styles.split(/\s*;\s*/g).map(function(style) { - return !style ? '' : prefix + style; + // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x + // Mentioned browsers need a -webkit- prefix for transitions & animations. + var prefixedStyles = styles.split(/\s*;\s*/g) + .filter(function(style) { + return style && /^(?:transition|animation)\b/.test(style); + }) + .map(function(style) { + return '-webkit-' + style; }).join('; '); - this.addRule(selector, prefixedStyles); - } + this.addRule(selector, prefixedStyles); this.addRule(selector, styles); }, diff --git a/test/ng/snifferSpec.js b/test/ng/snifferSpec.js index 7216bc005b66..9ab3a7315ebe 100644 --- a/test/ng/snifferSpec.js +++ b/test/ng/snifferSpec.js @@ -172,39 +172,6 @@ describe('$sniffer', function() { }); - describe('vendorPrefix', function() { - it('should return the correct vendor prefix based on the browser', function() { - inject(function($sniffer, $window) { - var expectedPrefix; - var ua = $window.navigator.userAgent.toLowerCase(); - if (/edge/i.test(ua)) { - expectedPrefix = 'Ms'; - } else if (/chrome/i.test(ua) || /safari/i.test(ua) || /webkit/i.test(ua)) { - expectedPrefix = 'Webkit'; - } else if (/firefox/i.test(ua)) { - expectedPrefix = 'Moz'; - } else if (/ie/i.test(ua) || /trident/i.test(ua)) { - expectedPrefix = 'Ms'; - } - expect($sniffer.vendorPrefix).toBe(expectedPrefix); - }); - }); - - - it('should still work for an older version of Webkit', function() { - var mockDocument = { - body: { - style: { - WebkitOpacity: '0' - } - } - }; - - expect(sniffer({}, mockDocument).vendorPrefix).toBe('webkit'); - }); - }); - - describe('animations', function() { it('should be either true or false', inject(function($sniffer) { expect($sniffer.animations).toBeDefined(); @@ -222,13 +189,12 @@ describe('$sniffer', function() { }); - it('should be true with vendor-specific animations', function() { + it('should be true with -webkit-prefixed animations', function() { var animationStyle = 'some_animation 2s linear'; var mockDocument = { body: { style: { - WebkitAnimation: animationStyle, - MozAnimation: animationStyle + webkitAnimation: animationStyle } } }; @@ -299,13 +265,12 @@ describe('$sniffer', function() { }); - it('should be true with vendor-specific transitions', function() { + it('should be true with -webkit-prefixed transitions', function() { var transitionStyle = '1s linear all'; var mockDocument = { body: { style: { - WebkitTransition: transitionStyle, - MozTransition: transitionStyle + webkitTransition: transitionStyle } } }; diff --git a/test/ngAnimate/animateCssSpec.js b/test/ngAnimate/animateCssSpec.js index b0e7cf58faef..079716f8eacf 100644 --- a/test/ngAnimate/animateCssSpec.js +++ b/test/ngAnimate/animateCssSpec.js @@ -16,8 +16,8 @@ describe('ngAnimate $animateCss', function() { } function getPossiblyPrefixedStyleValue(element, styleProp) { - var value = element.css(prefix + styleProp); - if (isUndefined(value)) value = element.css(styleProp); + var value = element.css(styleProp); + if (isUndefined(value)) value = element.css('-webkit-' + styleProp); return value; } @@ -40,11 +40,10 @@ describe('ngAnimate $animateCss', function() { color: 'blue' }; - var ss, prefix, triggerAnimationStartFrame; + var ss, triggerAnimationStartFrame; beforeEach(module(function() { return function($document, $sniffer, $$rAF, $animate) { - prefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; - ss = createMockStyleSheet($document, prefix); + ss = createMockStyleSheet($document); $animate.enabled(true); triggerAnimationStartFrame = function() { @@ -873,8 +872,8 @@ describe('ngAnimate $animateCss', function() { angular.element($document[0].body).append($rootElement); - ss.addRule('.ng-enter-stagger', prefix + 'animation-delay:0.2s'); - ss.addRule('.transition-animation', 'transition:2s 5s linear all;'); + ss.addPossiblyPrefixedRule('.ng-enter-stagger', 'animation-delay:0.2s'); + ss.addPossiblyPrefixedRule('.transition-animation', 'transition:2s 5s linear all;'); for (var i = 0; i < 5; i++) { var element = angular.element('
'); @@ -2508,7 +2507,7 @@ describe('ngAnimate $animateCss', function() { } }, function(testDetailsFactory) { inject(function($animateCss, $rootElement) { - var testDetails = testDetailsFactory(prefix); + var testDetails = testDetailsFactory(); ss.addPossiblyPrefixedRule('.ng-enter', testDetails.css); var options = { From f5f802c6e68a3d4e22f5e7725057652519d15e60 Mon Sep 17 00:00:00 2001 From: laranhee Date: Thu, 20 Oct 2016 11:37:04 +0900 Subject: [PATCH 0085/1014] docs($rootScope): add missing round bracket Closes #15299 --- src/ng/rootScope.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 618c7b93872c..cd53ab4dc552 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -285,7 +285,7 @@ function $RootScopeProvider() { * $digest()} and should return the value that will be watched. (`watchExpression` should not change * its value when executed multiple times with the same input because it may be executed multiple * times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be - * [idempotent](http://en.wikipedia.org/wiki/Idempotence). + * [idempotent](http://en.wikipedia.org/wiki/Idempotence).) * - The `listener` is called only when the value from the current `watchExpression` and the * previous call to `watchExpression` are not equal (with the exception of the initial run, * see below). Inequality is determined according to reference inequality, From 828f8a63b588003da426405083f56c8bfaacb450 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Sat, 8 Oct 2016 12:23:44 -0700 Subject: [PATCH 0086/1014] docs($controller): deprecate the use of $controllerProvider#allowGlobals Closes #15230 --- src/ng/controller.js | 5 ++++- src/ng/directive/ngController.js | 2 +- src/ngMock/angular-mocks.js | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ng/controller.js b/src/ng/controller.js index f6c4a7c5abd1..14b6c17beac0 100644 --- a/src/ng/controller.js +++ b/src/ng/controller.js @@ -59,6 +59,9 @@ function $ControllerProvider() { * @ngdoc method * @name $controllerProvider#allowGlobals * @description If called, allows `$controller` to find controller constructors on `window` + * + * @deprecated + * This method of finding controllers has been deprecated. This will be removed in 1.7. */ this.allowGlobals = function() { globals = true; @@ -79,7 +82,7 @@ function $ControllerProvider() { * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global - * `window` object (not recommended) + * `window` object (deprecated, not recommended) * * The string can use the `controller as property` syntax, where the controller instance is published * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this diff --git a/src/ng/directive/ngController.js b/src/ng/directive/ngController.js index cd3bfed63a4c..d31b2548920d 100644 --- a/src/ng/directive/ngController.js +++ b/src/ng/directive/ngController.js @@ -33,7 +33,7 @@ * * If the current `$controllerProvider` is configured to use globals (via * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may - * also be the name of a globally accessible constructor function (not recommended). + * also be the name of a globally accessible constructor function (deprecated, not recommended). * * @example * Here is a simple form for editing user contact information. Adding, removing, clearing, and diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index e112f2f4053d..d4cf61ed5647 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -2225,7 +2225,7 @@ angular.mock.$RootElementProvider = function() { * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global - * `window` object (not recommended) + * `window` object (deprecated, not recommended) * * The string can use the `controller as property` syntax, where the controller instance is published * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this From 41034bb41beb6df132f677b83d6518df38827408 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Thu, 20 Oct 2016 01:00:32 -0700 Subject: [PATCH 0087/1014] test($compile): ensure equal but different instance changes are detected in onChanges Closes #15300 --- test/ng/compileSpec.js | 49 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 544baf8a74ca..4c4f1f589a2f 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -5453,7 +5453,7 @@ describe('$compile', function() { this.$onChanges = function(changes) { if (changes.input) { - log.push(['$onChanges', changes.input]); + log.push(['$onChanges', copy(changes.input)]); } }; } @@ -5482,6 +5482,53 @@ describe('$compile', function() { }); }); + it('should not update isolate again after $onInit if outer object reference has not changed', function() { + module('owComponentTest'); + inject(function() { + $rootScope.name = ['outer']; + compile(''); + + expect($rootScope.name).toEqual(['outer']); + expect(component.input).toEqual('$onInit'); + + $rootScope.name[0] = 'inner'; + $rootScope.$digest(); + + expect($rootScope.name).toEqual(['inner']); + expect(component.input).toEqual('$onInit'); + + expect(log).toEqual([ + 'constructor', + ['$onChanges', jasmine.objectContaining({ currentValue: ['outer'] })], + '$onInit' + ]); + }); + }); + + it('should update isolate again after $onInit if outer object reference changes even if equal', function() { + module('owComponentTest'); + inject(function() { + $rootScope.name = ['outer']; + compile(''); + + expect($rootScope.name).toEqual(['outer']); + expect(component.input).toEqual('$onInit'); + + $rootScope.name = ['outer']; + $rootScope.$digest(); + + expect($rootScope.name).toEqual(['outer']); + expect(component.input).toEqual(['outer']); + + expect(log).toEqual([ + 'constructor', + ['$onChanges', jasmine.objectContaining({ currentValue: ['outer'] })], + '$onInit', + ['$onChanges', jasmine.objectContaining({ previousValue: ['outer'], currentValue: ['outer'] })] + ]); + }); + }); + it('should not update isolate again after $onInit if outer is a literal', function() { module('owComponentTest'); inject(function() { From 586e2acb269016a0fee66ac33f4a385f631afad0 Mon Sep 17 00:00:00 2001 From: Jonathan Yates Date: Wed, 19 Oct 2016 09:33:15 -0700 Subject: [PATCH 0088/1014] fix($compile): clean up `@`-binding observers when re-assigning bindings Fixes #15268 Closes #15298 --- src/ng/compile.js | 3 ++- test/ng/compileSpec.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index bc1cae5bfca6..6d988aa698f9 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -3434,7 +3434,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!optional && !hasOwnProperty.call(attrs, attrName)) { destination[scopeName] = attrs[attrName] = undefined; } - attrs.$observe(attrName, function(value) { + removeWatch = attrs.$observe(attrName, function(value) { if (isString(value) || isBoolean(value)) { var oldValue = destination[scopeName]; recordChanges(scopeName, value, oldValue); @@ -3453,6 +3453,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { destination[scopeName] = lastValue; } initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + removeWatchCollection.push(removeWatch); break; case '=': diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 4c4f1f589a2f..4b85144bf8dd 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -4378,6 +4378,37 @@ describe('$compile', function() { }); }); + it('should clean up `@`-binding observers when re-assigning bindings', function() { + var constructorSpy = jasmine.createSpy('constructor'); + var prototypeSpy = jasmine.createSpy('prototype'); + + function TestController() { + return {$onChanges: constructorSpy}; + } + TestController.prototype.$onChanges = prototypeSpy; + + module(function($compileProvider) { + $compileProvider.component('test', { + bindings: {attr: '@'}, + controller: TestController + }); + }); + + inject(function($compile, $rootScope) { + var template = ''; + $rootScope.a = 'foo'; + + element = $compile(template)($rootScope); + $rootScope.$digest(); + expect(constructorSpy).toHaveBeenCalled(); + expect(prototypeSpy).not.toHaveBeenCalled(); + + constructorSpy.calls.reset(); + $rootScope.$apply('a = "bar"'); + expect(constructorSpy).toHaveBeenCalled(); + expect(prototypeSpy).not.toHaveBeenCalled(); + }); + }); it('should not call `$onChanges` twice even when the initial value is `NaN`', function() { var onChangesSpy = jasmine.createSpy('$onChanges'); From a6118dfda45b2911c305d344a7c6a4a9411e2d13 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Fri, 21 Oct 2016 11:58:31 +0300 Subject: [PATCH 0089/1014] chore(ng-closure-runner): upgrade to version 0.2.4 Version 0.2.4's `minErr` implementation is up to date with the one in core. Fixes #14971 Closes #15307 --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 1d6ee278b5f3..95db7a55a742 100644 --- a/bower.json +++ b/bower.json @@ -6,6 +6,6 @@ "jquery-2.2": "jquery#2.2.4", "jquery-2.1": "jquery#2.1.4", "closure-compiler": "https://dl.google.com/closure-compiler/compiler-20140814.zip", - "ng-closure-runner": "https://raw.github.com/angular/ng-closure-runner/v0.2.3/assets/ng-closure-runner.zip" + "ng-closure-runner": "https://raw.github.com/angular/ng-closure-runner/v0.2.4/assets/ng-closure-runner.zip" } } From eeb9ef09f9972ec1850549f06a324f150b732873 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 21 Oct 2016 08:58:36 +0100 Subject: [PATCH 0090/1014] chore(gruntfile): check the node version before starting We specify the node version that is required to run the build in the `.nvmrc` file. So let's check that the current node version satisfies this and report a helpful message if it is not. --- Gruntfile.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Gruntfile.js b/Gruntfile.js index 5b31c8ea24be..c338ae85f9da 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,6 +9,14 @@ var versionInfo = require('./lib/versions/version-info'); var path = require('path'); var e2e = require('./test/e2e/tools'); +var semver = require('semver'); +var fs = require('fs'); + +var useNodeVersion = fs.readFileSync('.nvmrc', 'utf8'); +if (!semver.satisfies(process.version, useNodeVersion)) { + throw new Error('Invalid node version; please use node v' + useNodeVersion); +} + module.exports = function(grunt) { //grunt plugins require('load-grunt-tasks')(grunt); From 21ac2c42eab490a1fb2235c694ae8d934c5e9b36 Mon Sep 17 00:00:00 2001 From: emed Date: Tue, 25 Oct 2016 10:11:22 -0600 Subject: [PATCH 0091/1014] docs(error/ueoe): add another possible cause Mention unescaped quotes as another possible cause for this error. Closes #15313 --- docs/content/error/$parse/ueoe.ngdoc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/content/error/$parse/ueoe.ngdoc b/docs/content/error/$parse/ueoe.ngdoc index 97535a317416..e96df0322377 100644 --- a/docs/content/error/$parse/ueoe.ngdoc +++ b/docs/content/error/$parse/ueoe.ngdoc @@ -4,6 +4,9 @@ @description Occurs when an expression is missing tokens at the end of the expression. -For example, forgetting a closing bracket in an expression will trigger this error. -To resolve, learn more about {@link guide/expression Angular expressions}, identify the error and fix the expression's syntax. +For example, forgetting to close a bracket or failing to properly escape quotes in an expression +will trigger this error. + +To resolve, learn more about {@link guide/expression Angular expressions}, identify the error and +fix the expression's syntax. From 6a3374968680ccd809b62ba22b834389ebba99bc Mon Sep 17 00:00:00 2001 From: Allan Watson Date: Thu, 20 Oct 2016 13:49:18 -0400 Subject: [PATCH 0092/1014] docs(orderBy): clarify behavior of default comparator wrt `null` Document how `orderBy`'s default comparator handles `null` values. Fixes #15293 Closes #15304 --- docs/content/guide/migration.ngdoc | 5 ++++- src/ng/filter/orderBy.js | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/content/guide/migration.ngdoc b/docs/content/guide/migration.ngdoc index 6ddb29cb5a25..db4b5754639f 100644 --- a/docs/content/guide/migration.ngdoc +++ b/docs/content/guide/migration.ngdoc @@ -1125,7 +1125,11 @@ and "$dependentProvider" would have actually accomplished something and changed app. This is no longer possible within a single module. +### Filters (`orderBy`) +- due to [a097aa95](https://github.com/angular/angular.js/commit/a097aa95b7c78beab6d1b7d521c25f7d9d7843d9), + `orderBy` now treats `null` values (which in JavaScript have type `object`) as having a string + represenation of `'null'`. ### Animation (`ngAnimate`) @@ -1229,7 +1233,6 @@ or simply use: - ## Migrating from 1.0 to 1.2 diff --git a/src/ng/filter/orderBy.js b/src/ng/filter/orderBy.js index a86f3a82ab90..f65821a98e6a 100644 --- a/src/ng/filter/orderBy.js +++ b/src/ng/filter/orderBy.js @@ -86,6 +86,9 @@ * * **Note:** If you notice numbers not being sorted as expected, make sure they are actually being * saved as numbers and not strings. + * **Note:** For the purpose of sorting, `null` values are treated as the string `'null'` (i.e. + * `type: 'string'`, `value: 'null'`). This may cause unexpected sort order relative to + * other values. * * @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort. * @param {(Function|string|Array.)=} expression - A predicate (or list of From 491d23ed571c514cdd44479166d843501813ac91 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 26 Oct 2016 17:21:57 +0100 Subject: [PATCH 0093/1014] chore(deps): add changez --- npm-shrinkwrap.clean.json | 794 ++++++++++++++++++++++ npm-shrinkwrap.json | 1325 ++++++++++++++++++++++++++++++++++++- package.json | 4 +- 3 files changed, 2086 insertions(+), 37 deletions(-) diff --git a/npm-shrinkwrap.clean.json b/npm-shrinkwrap.clean.json index 16f7fc7818c4..4a38d100b7bc 100644 --- a/npm-shrinkwrap.clean.json +++ b/npm-shrinkwrap.clean.json @@ -2212,6 +2212,800 @@ "canonical-path": { "version": "0.0.2" }, + "changez": { + "version": "2.1.1", + "dependencies": { + "commander": { + "version": "2.9.0", + "dependencies": { + "graceful-readlink": { + "version": "1.0.1" + } + } + }, + "find-package": { + "version": "1.0.0", + "dependencies": { + "parents": { + "version": "1.0.1", + "dependencies": { + "path-platform": { + "version": "0.11.15" + } + } + } + } + }, + "nunjucks": { + "version": "2.5.2", + "dependencies": { + "asap": { + "version": "2.0.5" + }, + "chokidar": { + "version": "1.6.1", + "dependencies": { + "anymatch": { + "version": "1.3.0", + "dependencies": { + "arrify": { + "version": "1.0.1" + }, + "micromatch": { + "version": "2.3.11", + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "dependencies": { + "arr-flatten": { + "version": "1.0.1" + } + } + }, + "array-unique": { + "version": "0.2.1" + }, + "braces": { + "version": "1.8.5", + "dependencies": { + "expand-range": { + "version": "1.8.2", + "dependencies": { + "fill-range": { + "version": "2.2.3", + "dependencies": { + "is-number": { + "version": "2.1.0" + }, + "isobject": { + "version": "2.1.0", + "dependencies": { + "isarray": { + "version": "1.0.0" + } + } + }, + "randomatic": { + "version": "1.1.5" + }, + "repeat-string": { + "version": "1.6.1" + } + } + } + } + }, + "preserve": { + "version": "0.2.0" + }, + "repeat-element": { + "version": "1.1.2" + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "dependencies": { + "is-posix-bracket": { + "version": "0.1.1" + } + } + }, + "extglob": { + "version": "0.3.2" + }, + "filename-regex": { + "version": "2.0.0" + }, + "is-extglob": { + "version": "1.0.0" + }, + "kind-of": { + "version": "3.0.4", + "dependencies": { + "is-buffer": { + "version": "1.1.4" + } + } + }, + "normalize-path": { + "version": "2.0.1" + }, + "object.omit": { + "version": "2.0.0", + "dependencies": { + "for-own": { + "version": "0.1.4", + "dependencies": { + "for-in": { + "version": "0.1.6" + } + } + }, + "is-extendable": { + "version": "0.1.1" + } + } + }, + "parse-glob": { + "version": "3.0.4", + "dependencies": { + "glob-base": { + "version": "0.3.0" + }, + "is-dotfile": { + "version": "1.0.2" + } + } + }, + "regex-cache": { + "version": "0.4.3", + "dependencies": { + "is-equal-shallow": { + "version": "0.1.3" + }, + "is-primitive": { + "version": "2.0.0" + } + } + } + } + } + } + }, + "async-each": { + "version": "1.0.1" + }, + "glob-parent": { + "version": "2.0.0" + }, + "inherits": { + "version": "2.0.3" + }, + "is-binary-path": { + "version": "1.0.1", + "dependencies": { + "binary-extensions": { + "version": "1.7.0" + } + } + }, + "is-glob": { + "version": "2.0.1", + "dependencies": { + "is-extglob": { + "version": "1.0.0" + } + } + }, + "path-is-absolute": { + "version": "1.0.1" + }, + "readdirp": { + "version": "2.1.0", + "dependencies": { + "graceful-fs": { + "version": "4.1.9" + }, + "minimatch": { + "version": "3.0.3", + "dependencies": { + "brace-expansion": { + "version": "1.1.6", + "dependencies": { + "balanced-match": { + "version": "0.4.2" + }, + "concat-map": { + "version": "0.0.1" + } + } + } + } + }, + "readable-stream": { + "version": "2.1.5", + "dependencies": { + "buffer-shims": { + "version": "1.0.0" + }, + "core-util-is": { + "version": "1.0.2" + }, + "isarray": { + "version": "1.0.0" + }, + "process-nextick-args": { + "version": "1.0.7" + }, + "string_decoder": { + "version": "0.10.31" + }, + "util-deprecate": { + "version": "1.0.2" + } + } + }, + "set-immediate-shim": { + "version": "1.0.1" + } + } + }, + "fsevents": { + "version": "1.0.14", + "dependencies": { + "nan": { + "version": "2.4.0" + }, + "node-pre-gyp": { + "version": "0.6.29" + }, + "abbrev": { + "version": "1.0.9" + }, + "ansi-styles": { + "version": "2.2.1" + }, + "aproba": { + "version": "1.0.4" + }, + "ansi-regex": { + "version": "2.0.0" + }, + "are-we-there-yet": { + "version": "1.1.2" + }, + "asn1": { + "version": "0.2.3" + }, + "assert-plus": { + "version": "0.2.0" + }, + "aws-sign2": { + "version": "0.6.0" + }, + "async": { + "version": "1.5.2" + }, + "aws4": { + "version": "1.4.1" + }, + "block-stream": { + "version": "0.0.9" + }, + "balanced-match": { + "version": "0.4.2" + }, + "boom": { + "version": "2.10.1" + }, + "brace-expansion": { + "version": "1.1.5" + }, + "caseless": { + "version": "0.11.0" + }, + "buffer-shims": { + "version": "1.0.0" + }, + "chalk": { + "version": "1.1.3" + }, + "code-point-at": { + "version": "1.0.0" + }, + "commander": { + "version": "2.9.0" + }, + "combined-stream": { + "version": "1.0.5" + }, + "concat-map": { + "version": "0.0.1" + }, + "cryptiles": { + "version": "2.0.5" + }, + "console-control-strings": { + "version": "1.1.0" + }, + "debug": { + "version": "2.2.0" + }, + "core-util-is": { + "version": "1.0.2" + }, + "deep-extend": { + "version": "0.4.1" + }, + "delegates": { + "version": "1.0.0" + }, + "ecc-jsbn": { + "version": "0.1.1" + }, + "delayed-stream": { + "version": "1.0.0" + }, + "extend": { + "version": "3.0.0" + }, + "escape-string-regexp": { + "version": "1.0.5" + }, + "extsprintf": { + "version": "1.0.2" + }, + "forever-agent": { + "version": "0.6.1" + }, + "fstream": { + "version": "1.0.10" + }, + "gauge": { + "version": "2.6.0" + }, + "fs.realpath": { + "version": "1.0.0" + }, + "form-data": { + "version": "1.0.0-rc4" + }, + "fstream-ignore": { + "version": "1.0.5" + }, + "generate-object-property": { + "version": "1.2.0" + }, + "generate-function": { + "version": "2.0.0" + }, + "graceful-fs": { + "version": "4.1.4" + }, + "graceful-readlink": { + "version": "1.0.1" + }, + "glob": { + "version": "7.0.5" + }, + "har-validator": { + "version": "2.0.6" + }, + "has-unicode": { + "version": "2.0.1" + }, + "has-ansi": { + "version": "2.0.0" + }, + "has-color": { + "version": "0.1.7" + }, + "hoek": { + "version": "2.16.3" + }, + "hawk": { + "version": "3.1.3" + }, + "inherits": { + "version": "2.0.1" + }, + "ini": { + "version": "1.3.4" + }, + "http-signature": { + "version": "1.1.1" + }, + "is-my-json-valid": { + "version": "2.13.1" + }, + "is-fullwidth-code-point": { + "version": "1.0.0" + }, + "is-typedarray": { + "version": "1.0.0" + }, + "inflight": { + "version": "1.0.5" + }, + "isarray": { + "version": "1.0.0" + }, + "is-property": { + "version": "1.0.2" + }, + "jodid25519": { + "version": "1.0.2" + }, + "isstream": { + "version": "0.1.2" + }, + "jsonpointer": { + "version": "2.0.0" + }, + "jsbn": { + "version": "0.1.0" + }, + "json-schema": { + "version": "0.2.2" + }, + "json-stringify-safe": { + "version": "5.0.1" + }, + "mime-db": { + "version": "1.23.0" + }, + "jsprim": { + "version": "1.3.0" + }, + "minimist": { + "version": "0.0.8" + }, + "mime-types": { + "version": "2.1.11" + }, + "minimatch": { + "version": "3.0.2" + }, + "ms": { + "version": "0.7.1" + }, + "mkdirp": { + "version": "0.5.1" + }, + "nopt": { + "version": "3.0.6" + }, + "node-uuid": { + "version": "1.4.7" + }, + "npmlog": { + "version": "3.1.2" + }, + "oauth-sign": { + "version": "0.8.2" + }, + "number-is-nan": { + "version": "1.0.0" + }, + "object-assign": { + "version": "4.1.0" + }, + "pinkie": { + "version": "2.0.4" + }, + "pinkie-promise": { + "version": "2.0.1" + }, + "process-nextick-args": { + "version": "1.0.7" + }, + "once": { + "version": "1.3.3" + }, + "path-is-absolute": { + "version": "1.0.0" + }, + "qs": { + "version": "6.2.0" + }, + "readable-stream": { + "version": "2.1.4" + }, + "request": { + "version": "2.73.0" + }, + "rimraf": { + "version": "2.5.3" + }, + "semver": { + "version": "5.2.0" + }, + "signal-exit": { + "version": "3.0.0" + }, + "set-blocking": { + "version": "2.0.0" + }, + "string_decoder": { + "version": "0.10.31" + }, + "strip-ansi": { + "version": "3.0.1" + }, + "sntp": { + "version": "1.0.9" + }, + "stringstream": { + "version": "0.0.5" + }, + "string-width": { + "version": "1.0.1" + }, + "tar": { + "version": "2.2.1" + }, + "tunnel-agent": { + "version": "0.4.3" + }, + "strip-json-comments": { + "version": "1.0.4" + }, + "tar-pack": { + "version": "3.1.4" + }, + "tough-cookie": { + "version": "2.2.2" + }, + "supports-color": { + "version": "2.0.0" + }, + "uid-number": { + "version": "0.0.6" + }, + "util-deprecate": { + "version": "1.0.2" + }, + "tweetnacl": { + "version": "0.13.3" + }, + "verror": { + "version": "1.3.6" + }, + "wide-align": { + "version": "1.1.0" + }, + "xtend": { + "version": "4.0.1" + }, + "wrappy": { + "version": "1.0.2" + }, + "bl": { + "version": "1.1.2", + "dependencies": { + "readable-stream": { + "version": "2.0.6" + } + } + }, + "sshpk": { + "version": "1.8.3", + "dependencies": { + "assert-plus": { + "version": "1.0.0" + } + } + }, + "dashdash": { + "version": "1.14.0", + "dependencies": { + "assert-plus": { + "version": "1.0.0" + } + } + }, + "getpass": { + "version": "0.1.6", + "dependencies": { + "assert-plus": { + "version": "1.0.0" + } + } + }, + "rc": { + "version": "1.1.6", + "dependencies": { + "minimist": { + "version": "1.2.0" + } + } + } + } + } + } + }, + "yargs": { + "version": "3.32.0", + "dependencies": { + "camelcase": { + "version": "2.1.1" + }, + "cliui": { + "version": "3.2.0", + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "dependencies": { + "ansi-regex": { + "version": "2.0.0" + } + } + }, + "wrap-ansi": { + "version": "2.0.0" + } + } + }, + "decamelize": { + "version": "1.2.0" + }, + "os-locale": { + "version": "1.4.0", + "dependencies": { + "lcid": { + "version": "1.0.0", + "dependencies": { + "invert-kv": { + "version": "1.0.0" + } + } + } + } + }, + "string-width": { + "version": "1.0.2", + "dependencies": { + "code-point-at": { + "version": "1.0.1", + "dependencies": { + "number-is-nan": { + "version": "1.0.1" + } + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "dependencies": { + "number-is-nan": { + "version": "1.0.1" + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "dependencies": { + "ansi-regex": { + "version": "2.0.0" + } + } + } + } + }, + "window-size": { + "version": "0.1.4" + }, + "y18n": { + "version": "3.2.1" + } + } + } + } + }, + "shelljs": { + "version": "0.7.4", + "dependencies": { + "glob": { + "version": "7.1.1", + "dependencies": { + "fs.realpath": { + "version": "1.0.0" + }, + "inflight": { + "version": "1.0.6", + "dependencies": { + "wrappy": { + "version": "1.0.2" + } + } + }, + "inherits": { + "version": "2.0.3" + }, + "minimatch": { + "version": "3.0.3", + "dependencies": { + "brace-expansion": { + "version": "1.1.6", + "dependencies": { + "balanced-match": { + "version": "0.4.2" + }, + "concat-map": { + "version": "0.0.1" + } + } + } + } + }, + "once": { + "version": "1.4.0", + "dependencies": { + "wrappy": { + "version": "1.0.2" + } + } + }, + "path-is-absolute": { + "version": "1.0.1" + } + } + }, + "interpret": { + "version": "1.0.1" + }, + "rechoir": { + "version": "0.6.2", + "dependencies": { + "resolve": { + "version": "1.1.7" + } + } + } + } + }, + "simple-node-logger": { + "version": "0.93.12", + "dependencies": { + "lodash": { + "version": "4.16.4" + }, + "moment": { + "version": "2.15.2" + }, + "path": { + "version": "0.12.7", + "dependencies": { + "process": { + "version": "0.11.9" + } + } + }, + "util": { + "version": "0.10.3", + "dependencies": { + "inherits": { + "version": "2.0.1" + } + } + } + } + } + } + }, + "changez-angular": { + "version": "2.1.0", + "dependencies": { + "nunjucks-date": { + "version": "1.2.0", + "dependencies": { + "moment": { + "version": "2.15.2" + } + } + } + } + }, "cheerio": { "version": "0.17.0", "dependencies": { diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 4023e8f04610..2344bfb3726a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -3402,6 +3402,1259 @@ "from": "https://registry.npmjs.org/canonical-path/-/canonical-path-0.0.2.tgz", "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-0.0.2.tgz" }, + "changez": { + "version": "2.1.1", + "from": "changez@2.1.1", + "dependencies": { + "commander": { + "version": "2.9.0", + "from": "commander@>=2.9.0 <3.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "dependencies": { + "graceful-readlink": { + "version": "1.0.1", + "from": "graceful-readlink@>=1.0.0", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" + } + } + }, + "find-package": { + "version": "1.0.0", + "from": "find-package@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/find-package/-/find-package-1.0.0.tgz", + "dependencies": { + "parents": { + "version": "1.0.1", + "from": "parents@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "dependencies": { + "path-platform": { + "version": "0.11.15", + "from": "path-platform@>=0.11.15 <0.12.0", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz" + } + } + } + } + }, + "nunjucks": { + "version": "2.5.2", + "from": "nunjucks@>=2.5.2 <3.0.0", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-2.5.2.tgz", + "dependencies": { + "asap": { + "version": "2.0.5", + "from": "asap@>=2.0.3 <3.0.0", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz" + }, + "chokidar": { + "version": "1.6.1", + "from": "chokidar@>=1.6.0 <2.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.6.1.tgz", + "dependencies": { + "anymatch": { + "version": "1.3.0", + "from": "anymatch@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz", + "dependencies": { + "arrify": { + "version": "1.0.1", + "from": "arrify@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" + }, + "micromatch": { + "version": "2.3.11", + "from": "micromatch@>=2.1.5 <3.0.0", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "from": "arr-diff@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "dependencies": { + "arr-flatten": { + "version": "1.0.1", + "from": "arr-flatten@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.0.1.tgz" + } + } + }, + "array-unique": { + "version": "0.2.1", + "from": "array-unique@>=0.2.1 <0.3.0", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz" + }, + "braces": { + "version": "1.8.5", + "from": "braces@>=1.8.2 <2.0.0", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "dependencies": { + "expand-range": { + "version": "1.8.2", + "from": "expand-range@>=1.8.1 <2.0.0", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "dependencies": { + "fill-range": { + "version": "2.2.3", + "from": "fill-range@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "dependencies": { + "is-number": { + "version": "2.1.0", + "from": "is-number@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz" + }, + "isobject": { + "version": "2.1.0", + "from": "isobject@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "dependencies": { + "isarray": { + "version": "1.0.0", + "from": "isarray@1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + } + } + }, + "randomatic": { + "version": "1.1.5", + "from": "randomatic@>=1.1.3 <2.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.5.tgz" + }, + "repeat-string": { + "version": "1.6.1", + "from": "repeat-string@>=1.5.2 <2.0.0", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" + } + } + } + } + }, + "preserve": { + "version": "0.2.0", + "from": "preserve@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz" + }, + "repeat-element": { + "version": "1.1.2", + "from": "repeat-element@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz" + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "from": "expand-brackets@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "dependencies": { + "is-posix-bracket": { + "version": "0.1.1", + "from": "is-posix-bracket@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz" + } + } + }, + "extglob": { + "version": "0.3.2", + "from": "extglob@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz" + }, + "filename-regex": { + "version": "2.0.0", + "from": "filename-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.0.tgz" + }, + "is-extglob": { + "version": "1.0.0", + "from": "is-extglob@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz" + }, + "kind-of": { + "version": "3.0.4", + "from": "kind-of@>=3.0.2 <4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.0.4.tgz", + "dependencies": { + "is-buffer": { + "version": "1.1.4", + "from": "is-buffer@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.4.tgz" + } + } + }, + "normalize-path": { + "version": "2.0.1", + "from": "normalize-path@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.0.1.tgz" + }, + "object.omit": { + "version": "2.0.0", + "from": "object.omit@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.0.tgz", + "dependencies": { + "for-own": { + "version": "0.1.4", + "from": "for-own@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.4.tgz", + "dependencies": { + "for-in": { + "version": "0.1.6", + "from": "for-in@>=0.1.5 <0.2.0", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.6.tgz" + } + } + }, + "is-extendable": { + "version": "0.1.1", + "from": "is-extendable@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" + } + } + }, + "parse-glob": { + "version": "3.0.4", + "from": "parse-glob@>=3.0.4 <4.0.0", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "dependencies": { + "glob-base": { + "version": "0.3.0", + "from": "glob-base@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz" + }, + "is-dotfile": { + "version": "1.0.2", + "from": "is-dotfile@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.2.tgz" + } + } + }, + "regex-cache": { + "version": "0.4.3", + "from": "regex-cache@>=0.4.2 <0.5.0", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz", + "dependencies": { + "is-equal-shallow": { + "version": "0.1.3", + "from": "is-equal-shallow@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz" + }, + "is-primitive": { + "version": "2.0.0", + "from": "is-primitive@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz" + } + } + } + } + } + } + }, + "async-each": { + "version": "1.0.1", + "from": "async-each@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz" + }, + "glob-parent": { + "version": "2.0.0", + "from": "glob-parent@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz" + }, + "inherits": { + "version": "2.0.3", + "from": "inherits@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + }, + "is-binary-path": { + "version": "1.0.1", + "from": "is-binary-path@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "dependencies": { + "binary-extensions": { + "version": "1.7.0", + "from": "binary-extensions@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.7.0.tgz" + } + } + }, + "is-glob": { + "version": "2.0.1", + "from": "is-glob@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "from": "is-extglob@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz" + } + } + }, + "path-is-absolute": { + "version": "1.0.1", + "from": "path-is-absolute@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + }, + "readdirp": { + "version": "2.1.0", + "from": "readdirp@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "dependencies": { + "graceful-fs": { + "version": "4.1.9", + "from": "graceful-fs@>=4.1.2 <5.0.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.9.tgz" + }, + "minimatch": { + "version": "3.0.3", + "from": "minimatch@>=3.0.2 <4.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "dependencies": { + "brace-expansion": { + "version": "1.1.6", + "from": "brace-expansion@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "from": "balanced-match@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + }, + "readable-stream": { + "version": "2.1.5", + "from": "readable-stream@>=2.0.2 <3.0.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", + "dependencies": { + "buffer-shims": { + "version": "1.0.0", + "from": "buffer-shims@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz" + }, + "core-util-is": { + "version": "1.0.2", + "from": "core-util-is@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + }, + "process-nextick-args": { + "version": "1.0.7", + "from": "process-nextick-args@>=1.0.6 <1.1.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "util-deprecate": { + "version": "1.0.2", + "from": "util-deprecate@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } + } + }, + "set-immediate-shim": { + "version": "1.0.1", + "from": "set-immediate-shim@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz" + } + } + }, + "fsevents": { + "version": "1.0.14", + "from": "fsevents@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.0.14.tgz", + "dependencies": { + "nan": { + "version": "2.4.0", + "from": "nan@>=2.3.0 <3.0.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz" + }, + "node-pre-gyp": { + "version": "0.6.29", + "from": "node-pre-gyp@>=0.6.29 <0.7.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.29.tgz" + }, + "abbrev": { + "version": "1.0.9", + "from": "abbrev@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz" + }, + "ansi-styles": { + "version": "2.2.1", + "from": "ansi-styles@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" + }, + "aproba": { + "version": "1.0.4", + "from": "aproba@>=1.0.3 <2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.0.4.tgz" + }, + "ansi-regex": { + "version": "2.0.0", + "from": "ansi-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" + }, + "are-we-there-yet": { + "version": "1.1.2", + "from": "are-we-there-yet@>=1.1.2 <1.2.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz" + }, + "asn1": { + "version": "0.2.3", + "from": "asn1@>=0.2.3 <0.3.0", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" + }, + "assert-plus": { + "version": "0.2.0", + "from": "assert-plus@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" + }, + "aws-sign2": { + "version": "0.6.0", + "from": "aws-sign2@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" + }, + "async": { + "version": "1.5.2", + "from": "async@>=1.5.2 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + }, + "aws4": { + "version": "1.4.1", + "from": "aws4@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.4.1.tgz" + }, + "block-stream": { + "version": "0.0.9", + "from": "block-stream@*", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz" + }, + "balanced-match": { + "version": "0.4.2", + "from": "balanced-match@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" + }, + "boom": { + "version": "2.10.1", + "from": "boom@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" + }, + "brace-expansion": { + "version": "1.1.5", + "from": "brace-expansion@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.5.tgz" + }, + "caseless": { + "version": "0.11.0", + "from": "caseless@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" + }, + "buffer-shims": { + "version": "1.0.0", + "from": "buffer-shims@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz" + }, + "chalk": { + "version": "1.1.3", + "from": "chalk@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" + }, + "code-point-at": { + "version": "1.0.0", + "from": "code-point-at@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz" + }, + "commander": { + "version": "2.9.0", + "from": "commander@>=2.9.0 <3.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz" + }, + "combined-stream": { + "version": "1.0.5", + "from": "combined-stream@>=1.0.5 <1.1.0", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + }, + "cryptiles": { + "version": "2.0.5", + "from": "cryptiles@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" + }, + "console-control-strings": { + "version": "1.1.0", + "from": "console-control-strings@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" + }, + "debug": { + "version": "2.2.0", + "from": "debug@>=2.2.0 <2.3.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz" + }, + "core-util-is": { + "version": "1.0.2", + "from": "core-util-is@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + }, + "deep-extend": { + "version": "0.4.1", + "from": "deep-extend@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz" + }, + "delegates": { + "version": "1.0.0", + "from": "delegates@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" + }, + "ecc-jsbn": { + "version": "0.1.1", + "from": "ecc-jsbn@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz" + }, + "delayed-stream": { + "version": "1.0.0", + "from": "delayed-stream@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + }, + "extend": { + "version": "3.0.0", + "from": "extend@>=3.0.0 <3.1.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" + }, + "escape-string-regexp": { + "version": "1.0.5", + "from": "escape-string-regexp@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + }, + "extsprintf": { + "version": "1.0.2", + "from": "extsprintf@1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" + }, + "forever-agent": { + "version": "0.6.1", + "from": "forever-agent@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + }, + "fstream": { + "version": "1.0.10", + "from": "fstream@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.10.tgz" + }, + "gauge": { + "version": "2.6.0", + "from": "gauge@>=2.6.0 <2.7.0", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.6.0.tgz" + }, + "fs.realpath": { + "version": "1.0.0", + "from": "fs.realpath@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + }, + "form-data": { + "version": "1.0.0-rc4", + "from": "form-data@>=1.0.0-rc4 <1.1.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz" + }, + "fstream-ignore": { + "version": "1.0.5", + "from": "fstream-ignore@>=1.0.5 <1.1.0", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz" + }, + "generate-object-property": { + "version": "1.2.0", + "from": "generate-object-property@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz" + }, + "generate-function": { + "version": "2.0.0", + "from": "generate-function@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" + }, + "graceful-fs": { + "version": "4.1.4", + "from": "graceful-fs@>=4.1.2 <5.0.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz" + }, + "graceful-readlink": { + "version": "1.0.1", + "from": "graceful-readlink@>=1.0.0", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" + }, + "glob": { + "version": "7.0.5", + "from": "glob@>=7.0.5 <8.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz" + }, + "har-validator": { + "version": "2.0.6", + "from": "har-validator@>=2.0.6 <2.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz" + }, + "has-unicode": { + "version": "2.0.1", + "from": "has-unicode@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" + }, + "has-ansi": { + "version": "2.0.0", + "from": "has-ansi@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" + }, + "has-color": { + "version": "0.1.7", + "from": "has-color@>=0.1.7 <0.2.0", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz" + }, + "hoek": { + "version": "2.16.3", + "from": "hoek@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + }, + "hawk": { + "version": "3.1.3", + "from": "hawk@>=3.1.3 <3.2.0", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "ini": { + "version": "1.3.4", + "from": "ini@>=1.3.0 <1.4.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz" + }, + "http-signature": { + "version": "1.1.1", + "from": "http-signature@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" + }, + "is-my-json-valid": { + "version": "2.13.1", + "from": "is-my-json-valid@>=2.12.4 <3.0.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz" + }, + "is-typedarray": { + "version": "1.0.0", + "from": "is-typedarray@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + }, + "inflight": { + "version": "1.0.5", + "from": "inflight@>=1.0.4 <2.0.0", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + }, + "is-property": { + "version": "1.0.2", + "from": "is-property@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" + }, + "jodid25519": { + "version": "1.0.2", + "from": "jodid25519@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz" + }, + "isstream": { + "version": "0.1.2", + "from": "isstream@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + }, + "jsonpointer": { + "version": "2.0.0", + "from": "jsonpointer@2.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" + }, + "jsbn": { + "version": "0.1.0", + "from": "jsbn@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" + }, + "json-schema": { + "version": "0.2.2", + "from": "json-schema@0.2.2", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" + }, + "json-stringify-safe": { + "version": "5.0.1", + "from": "json-stringify-safe@>=5.0.1 <5.1.0", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + }, + "mime-db": { + "version": "1.23.0", + "from": "mime-db@>=1.23.0 <1.24.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" + }, + "jsprim": { + "version": "1.3.0", + "from": "jsprim@>=1.2.2 <2.0.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.0.tgz" + }, + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + }, + "mime-types": { + "version": "2.1.11", + "from": "mime-types@>=2.1.7 <2.2.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz" + }, + "minimatch": { + "version": "3.0.2", + "from": "minimatch@>=3.0.2 <4.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.2.tgz" + }, + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + }, + "nopt": { + "version": "3.0.6", + "from": "nopt@>=3.0.1 <3.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz" + }, + "node-uuid": { + "version": "1.4.7", + "from": "node-uuid@>=1.4.7 <1.5.0", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" + }, + "npmlog": { + "version": "3.1.2", + "from": "npmlog@>=3.1.2 <3.2.0", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-3.1.2.tgz" + }, + "oauth-sign": { + "version": "0.8.2", + "from": "oauth-sign@>=0.8.1 <0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz" + }, + "number-is-nan": { + "version": "1.0.0", + "from": "number-is-nan@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz" + }, + "object-assign": { + "version": "4.1.0", + "from": "object-assign@>=4.1.0 <5.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz" + }, + "pinkie": { + "version": "2.0.4", + "from": "pinkie@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + }, + "pinkie-promise": { + "version": "2.0.1", + "from": "pinkie-promise@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" + }, + "process-nextick-args": { + "version": "1.0.7", + "from": "process-nextick-args@>=1.0.6 <1.1.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" + }, + "once": { + "version": "1.3.3", + "from": "once@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz" + }, + "path-is-absolute": { + "version": "1.0.0", + "from": "path-is-absolute@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" + }, + "qs": { + "version": "6.2.0", + "from": "qs@>=6.2.0 <6.3.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.0.tgz" + }, + "readable-stream": { + "version": "2.1.4", + "from": "readable-stream@>=2.0.0 <3.0.0||>=1.1.13 <2.0.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz" + }, + "request": { + "version": "2.73.0", + "from": "request@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.73.0.tgz" + }, + "rimraf": { + "version": "2.5.3", + "from": "rimraf@>=2.5.0 <2.6.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.3.tgz" + }, + "semver": { + "version": "5.2.0", + "from": "semver@>=5.2.0 <5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.2.0.tgz" + }, + "signal-exit": { + "version": "3.0.0", + "from": "signal-exit@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.0.tgz" + }, + "set-blocking": { + "version": "2.0.0", + "from": "set-blocking@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "strip-ansi": { + "version": "3.0.1", + "from": "strip-ansi@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + }, + "sntp": { + "version": "1.0.9", + "from": "sntp@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" + }, + "stringstream": { + "version": "0.0.5", + "from": "stringstream@>=0.0.4 <0.1.0", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" + }, + "string-width": { + "version": "1.0.1", + "from": "string-width@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.1.tgz" + }, + "tar": { + "version": "2.2.1", + "from": "tar@>=2.2.0 <2.3.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz" + }, + "tunnel-agent": { + "version": "0.4.3", + "from": "tunnel-agent@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz" + }, + "strip-json-comments": { + "version": "1.0.4", + "from": "strip-json-comments@>=1.0.4 <1.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz" + }, + "tar-pack": { + "version": "3.1.4", + "from": "tar-pack@>=3.1.0 <3.2.0", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.1.4.tgz" + }, + "tough-cookie": { + "version": "2.2.2", + "from": "tough-cookie@>=2.2.0 <2.3.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz" + }, + "supports-color": { + "version": "2.0.0", + "from": "supports-color@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + }, + "uid-number": { + "version": "0.0.6", + "from": "uid-number@>=0.0.6 <0.1.0", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" + }, + "util-deprecate": { + "version": "1.0.2", + "from": "util-deprecate@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + }, + "tweetnacl": { + "version": "0.13.3", + "from": "tweetnacl@>=0.13.0 <0.14.0", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz" + }, + "verror": { + "version": "1.3.6", + "from": "verror@1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" + }, + "wide-align": { + "version": "1.1.0", + "from": "wide-align@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.0.tgz" + }, + "xtend": { + "version": "4.0.1", + "from": "xtend@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" + }, + "wrappy": { + "version": "1.0.2", + "from": "wrappy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + }, + "bl": { + "version": "1.1.2", + "from": "bl@>=1.1.2 <1.2.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", + "dependencies": { + "readable-stream": { + "version": "2.0.6", + "from": "readable-stream@>=2.0.5 <2.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" + } + } + }, + "sshpk": { + "version": "1.8.3", + "from": "sshpk@>=1.7.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.8.3.tgz", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + } + }, + "dashdash": { + "version": "1.14.0", + "from": "dashdash@>=1.12.0 <2.0.0", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.0.tgz", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + } + }, + "getpass": { + "version": "0.1.6", + "from": "getpass@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + } + }, + "rc": { + "version": "1.1.6", + "from": "rc@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz", + "dependencies": { + "minimist": { + "version": "1.2.0", + "from": "minimist@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" + } + } + } + } + } + } + }, + "yargs": { + "version": "3.32.0", + "from": "yargs@>=3.32.0 <4.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "dependencies": { + "camelcase": { + "version": "2.1.1", + "from": "camelcase@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz" + }, + "cliui": { + "version": "3.2.0", + "from": "cliui@>=3.0.3 <4.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "from": "strip-ansi@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "dependencies": { + "ansi-regex": { + "version": "2.0.0", + "from": "ansi-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" + } + } + }, + "wrap-ansi": { + "version": "2.0.0", + "from": "wrap-ansi@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.0.0.tgz" + } + } + }, + "decamelize": { + "version": "1.2.0", + "from": "decamelize@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" + }, + "os-locale": { + "version": "1.4.0", + "from": "os-locale@>=1.4.0 <2.0.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "dependencies": { + "lcid": { + "version": "1.0.0", + "from": "lcid@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "dependencies": { + "invert-kv": { + "version": "1.0.0", + "from": "invert-kv@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz" + } + } + } + } + }, + "string-width": { + "version": "1.0.2", + "from": "string-width@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "dependencies": { + "code-point-at": { + "version": "1.0.1", + "from": "code-point-at@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.1.tgz", + "dependencies": { + "number-is-nan": { + "version": "1.0.1", + "from": "number-is-nan@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" + } + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "dependencies": { + "number-is-nan": { + "version": "1.0.1", + "from": "number-is-nan@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "from": "strip-ansi@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "dependencies": { + "ansi-regex": { + "version": "2.0.0", + "from": "ansi-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" + } + } + } + } + }, + "window-size": { + "version": "0.1.4", + "from": "window-size@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz" + }, + "y18n": { + "version": "3.2.1", + "from": "y18n@>=3.2.0 <4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz" + } + } + } + } + }, + "shelljs": { + "version": "0.7.4", + "from": "shelljs@>=0.7.4 <0.8.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.4.tgz", + "dependencies": { + "glob": { + "version": "7.1.1", + "from": "glob@>=7.0.0 <8.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "dependencies": { + "fs.realpath": { + "version": "1.0.0", + "from": "fs.realpath@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + }, + "inflight": { + "version": "1.0.6", + "from": "inflight@>=1.0.4 <2.0.0", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + } + }, + "inherits": { + "version": "2.0.3", + "from": "inherits@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + }, + "minimatch": { + "version": "3.0.3", + "from": "minimatch@>=3.0.2 <4.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "dependencies": { + "brace-expansion": { + "version": "1.1.6", + "from": "brace-expansion@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "from": "balanced-match@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + }, + "once": { + "version": "1.4.0", + "from": "once@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + } + }, + "path-is-absolute": { + "version": "1.0.1", + "from": "path-is-absolute@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + } + } + }, + "interpret": { + "version": "1.0.1", + "from": "interpret@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.1.tgz" + }, + "rechoir": { + "version": "0.6.2", + "from": "rechoir@>=0.6.2 <0.7.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "dependencies": { + "resolve": { + "version": "1.1.7", + "from": "resolve@>=1.1.6 <2.0.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz" + } + } + } + } + }, + "simple-node-logger": { + "version": "0.93.12", + "from": "simple-node-logger@>=0.93.12 <0.94.0", + "resolved": "https://registry.npmjs.org/simple-node-logger/-/simple-node-logger-0.93.12.tgz", + "dependencies": { + "lodash": { + "version": "4.16.4", + "from": "lodash@>=4.5.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.4.tgz" + }, + "moment": { + "version": "2.15.2", + "from": "moment@>=2.8.4 <3.0.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.15.2.tgz" + }, + "path": { + "version": "0.12.7", + "from": "path@>=0.12.7 <0.13.0", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "dependencies": { + "process": { + "version": "0.11.9", + "from": "process@>=0.11.1 <0.12.0", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.9.tgz" + } + } + }, + "util": { + "version": "0.10.3", + "from": "util@>=0.10.3 <0.11.0", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + } + } + } + } + }, + "changez-angular": { + "version": "2.1.0", + "from": "changez-angular@2.1.0", + "resolved": "https://registry.npmjs.org/changez-angular/-/changez-angular-2.1.0.tgz", + "dependencies": { + "nunjucks-date": { + "version": "1.2.0", + "from": "nunjucks-date@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/nunjucks-date/-/nunjucks-date-1.2.0.tgz", + "dependencies": { + "moment": { + "version": "2.15.2", + "from": "moment@>=2.8.4 <3.0.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.15.2.tgz" + } + } + } + } + }, "cheerio": { "version": "0.17.0", "from": "https://registry.npmjs.org/cheerio/-/cheerio-0.17.0.tgz", @@ -4042,34 +5295,34 @@ }, "cross-spawn": { "version": "4.0.0", - "from": "cross-spawn@latest", + "from": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.0.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.0.tgz", "dependencies": { "lru-cache": { "version": "4.0.1", - "from": "lru-cache@>=4.0.1 <5.0.0", + "from": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.1.tgz", "dependencies": { "pseudomap": { "version": "1.0.2", - "from": "pseudomap@>=1.0.1 <2.0.0", + "from": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz" }, "yallist": { "version": "2.0.0", - "from": "yallist@>=2.0.0 <3.0.0", + "from": "https://registry.npmjs.org/yallist/-/yallist-2.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.0.0.tgz" } } }, "which": { "version": "1.2.10", - "from": "which@>=1.2.9 <2.0.0", + "from": "https://registry.npmjs.org/which/-/which-1.2.10.tgz", "resolved": "https://registry.npmjs.org/which/-/which-1.2.10.tgz", "dependencies": { "isexe": { "version": "1.1.2", - "from": "isexe@>=1.1.1 <2.0.0", + "from": "https://registry.npmjs.org/isexe/-/isexe-1.1.2.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-1.1.2.tgz" } } @@ -12282,39 +13535,39 @@ }, "log4js": { "version": "0.6.38", - "from": "log4js@>=0.6.27 <0.7.0", + "from": "https://registry.npmjs.com/log4js/-/log4js-0.6.38.tgz", "resolved": "https://registry.npmjs.com/log4js/-/log4js-0.6.38.tgz", "dependencies": { "readable-stream": { "version": "1.0.34", - "from": "readable-stream@>=1.0.2 <1.1.0", + "from": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "dependencies": { "core-util-is": { "version": "1.0.2", - "from": "core-util-is@>=1.0.0 <1.1.0", + "from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1", + "from": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@>=0.10.0 <0.11.0", + "from": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@>=2.0.1 <2.1.0", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } }, "semver": { "version": "4.3.6", - "from": "semver@>=4.3.3 <4.4.0", + "from": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz" } } @@ -13232,64 +14485,64 @@ }, "selenium-webdriver": { "version": "2.53.3", - "from": "selenium-webdriver@>=2.53.1 <3.0.0", + "from": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-2.53.3.tgz", "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-2.53.3.tgz", "dependencies": { "adm-zip": { "version": "0.4.4", - "from": "adm-zip@0.4.4", + "from": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.4.tgz", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.4.tgz" }, "rimraf": { "version": "2.5.4", - "from": "rimraf@>=2.2.8 <3.0.0", + "from": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", "dependencies": { "glob": { "version": "7.0.6", - "from": "glob@>=7.0.5 <8.0.0", + "from": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", "dependencies": { "fs.realpath": { "version": "1.0.0", - "from": "fs.realpath@>=1.0.0 <2.0.0", + "from": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" }, "inflight": { "version": "1.0.5", - "from": "inflight@>=1.0.4 <2.0.0", + "from": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@>=1.0.0 <2.0.0", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } }, "inherits": { "version": "2.0.1", - "from": "inherits@>=2.0.0 <3.0.0", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "minimatch": { "version": "3.0.3", - "from": "minimatch@>=3.0.2 <4.0.0", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", "dependencies": { "brace-expansion": { "version": "1.1.6", - "from": "brace-expansion@>=1.0.0 <2.0.0", + "from": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", "dependencies": { "balanced-match": { "version": "0.4.2", - "from": "balanced-match@>=0.4.1 <0.5.0", + "from": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" }, "concat-map": { "version": "0.0.1", - "from": "concat-map@0.0.1", + "from": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" } } @@ -13298,19 +14551,19 @@ }, "once": { "version": "1.3.3", - "from": "once@>=1.3.0 <2.0.0", + "from": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@>=1.0.0 <2.0.0", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } }, "path-is-absolute": { "version": "1.0.0", - "from": "path-is-absolute@>=1.0.0 <2.0.0", + "from": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" } } @@ -13319,39 +14572,39 @@ }, "tmp": { "version": "0.0.24", - "from": "tmp@0.0.24", + "from": "https://registry.npmjs.org/tmp/-/tmp-0.0.24.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.24.tgz" }, "ws": { "version": "1.1.1", - "from": "ws@>=1.0.1 <2.0.0", + "from": "https://registry.npmjs.org/ws/-/ws-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.1.tgz", "dependencies": { "options": { "version": "0.0.6", - "from": "options@>=0.0.5", + "from": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz" }, "ultron": { "version": "1.0.2", - "from": "ultron@>=1.0.0 <1.1.0", + "from": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz" } } }, "xml2js": { "version": "0.4.4", - "from": "xml2js@0.4.4", + "from": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", "dependencies": { "sax": { "version": "0.6.1", - "from": "sax@>=0.6.0 <0.7.0", + "from": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz" }, "xmlbuilder": { "version": "8.2.2", - "from": "xmlbuilder@>=1.0.0", + "from": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz" } } @@ -13575,7 +14828,7 @@ }, "shelljs": { "version": "0.3.0", - "from": "shelljs@0.3.0", + "from": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz" }, "sorted-object": { diff --git a/package.json b/package.json index 84c2718dca5e..f5a1c3a703e5 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "MIT", "branchVersion": "^1.5.0-beta.2", "branchPattern": "1.5.*", - "distTag": "beta", + "distTag": "next", "repository": { "type": "git", "url": "https://github.com/angular/angular.js.git" @@ -26,6 +26,8 @@ "bower": "~1.3.9", "browserstacktunnel-wrapper": "^1.4.2", "canonical-path": "0.0.2", + "changez": "^2.1.1", + "changez-angular": "^2.1.0", "cheerio": "^0.17.0", "commitizen": "^2.3.0", "cross-spawn": "^4.0.0", From 3c88c624463ff86896d2e4750ad75e152c87f5ed Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 26 Oct 2016 17:26:10 +0100 Subject: [PATCH 0094/1014] docs(CHANGELOG): add 1.6.0-rc.0 release notes --- CHANGELOG.md | 1078 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1078 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99fa22865764..21af6b80eab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,1081 @@ + +# 1.6.0-rc.0 bracing-vortex (2016-10-26) + +## Major notes +Please read the [Sandbox Removal Blog Post](http://angularjs.blogspot.com/2016/09/angular-16-expression-sandbox-removal.html). + +## Bug Fixes +- **input:** fix `step` validation for `input[type=number]`/`input[type=range]` ([081d06](https://github.com/angular/angular.js/commit/081d06ffd15c2c6c539ce97b5eb63fa8e2403818) [#15257](https://github.com/angular/angular.js/issues/15257)) +- **jqLite:** + - camelCase keys in `jqLite#data` ([fc0c11](https://github.com/angular/angular.js/commit/fc0c11db845d53061430b7f05e773dcb3fb5b860) [#15126](https://github.com/angular/angular.js/issues/15126)) + - align jqLite camelCasing logic with JQuery ([73050c](https://github.com/angular/angular.js/commit/73050cdda04675bfa6705dc841ddbbb6919eb048) [#7744](https://github.com/angular/angular.js/issues/7744)) +- **$parse:** + - treat falsy values as defined in assignment expressions ([4f44e0](https://github.com/angular/angular.js/commit/4f44e018948c45bfb07f0170de4f703d22778d71)) + - call once stable bind-once expressions with filter ([3b5751](https://github.com/angular/angular.js/commit/3b5751dce8d6c699dc76e47cfa544c30b38b9771)) + - Handle sign of `-undefined` consistently ([c1eaf3](https://github.com/angular/angular.js/commit/c1eaf3480b9a88e5309ff4931a720f3f62bd7606)) +- **ngModel:** treat synchronous validators as boolean always ([7bc71a](https://github.com/angular/angular.js/commit/7bc71adc63bb6bb609b44dd2d3ea8fb0cd3f300b) [#14734](https://github.com/angular/angular.js/issues/14734)) +- **$q:** treat thrown errors as regular rejections ([e13eea](https://github.com/angular/angular.js/commit/e13eeabd7e34a78becec06cfbe72c23f2dcb85f9) [#3174](https://github.com/angular/angular.js/issues/3174) [#15213](https://github.com/angular/angular.js/issues/15213)) +- **ngTransclude:** use fallback content if only whitespace is provided ([32aa7e](https://github.com/angular/angular.js/commit/32aa7e7395527624119e3917c54ee43b4d219301) [#15077](https://github.com/angular/angular.js/issues/15077)) +- **$compile:** + - don't throw tplrt error when there is a whitespace around a top-level comment ([76d3da](https://github.com/angular/angular.js/commit/76d3dafdeaf2f343d094b5a34ffb74adf64bb284) [#15108](https://github.com/angular/angular.js/issues/15108)) + - disallow linking the same element more than once ([1e1fbc](https://github.com/angular/angular.js/commit/1e1fbc75f5e20e8541f517a5cf6f30f8f2eed53f)) + - lower the $sce context for src on video, audio, and track. ([ad9a99](https://github.com/angular/angular.js/commit/ad9a99d6895e1c07c950f7141bb0edfc1d4aaf61)) + - correctly merge consecutive text nodes on IE11 ([13c252](https://github.com/angular/angular.js/commit/13c2522baf7c8f616b2efcaab4bffd54c8736591) [#14924](https://github.com/angular/angular.js/issues/14924)) + - secure `link[href]` as a `RESOURCE_URL`s in `$sce`. ([04cad4](https://github.com/angular/angular.js/commit/04cad41d26ebaf44b5ee0c29a152d61f235f3efa) [#14687](https://github.com/angular/angular.js/issues/14687)) + - don't add leading white-space in attributes for a specific merge case ([305ba1](https://github.com/angular/angular.js/commit/305ba1a3fb3529cb3fdf04c12ac03fbb4f634456)) + - don't trim white-space in attributes ([97bbf8](https://github.com/angular/angular.js/commit/97bbf86a1979d099802f0d631c17c54b87563b40) [#5513](https://github.com/angular/angular.js/issues/5513) [#5597](https://github.com/angular/angular.js/issues/5597)) + - move check for interpolation of on-event attributes to compile time ([b89c21](https://github.com/angular/angular.js/commit/b89c2181a9a165e06c027390164e08635ec449f4) [#13267](https://github.com/angular/angular.js/issues/13267)) +- **select:** + - add/remove selected attribute for selected/unselected options ([c75698](https://github.com/angular/angular.js/commit/c75698df55f5a026bcd7fcecbb9d4ff0bc3ebc3e)) + - don't register options when select has no ngModel ([e8c2e1](https://github.com/angular/angular.js/commit/e8c2e119758e58e18fe43932d09a8ff9f506aa9d)) + - handle model updates when options are manipulated ([47c15f](https://github.com/angular/angular.js/commit/47c15fbcc10f118170813021e8e605ffd263ad84)) + - remove workaround for Chrome bug ([87eff2](https://github.com/angular/angular.js/commit/87eff27e971414fb163e2b5a7cfe78cb097a1951)) +- **select, ngOptions:** make the handling of unknown / empty options consistent ([2785ad](https://github.com/angular/angular.js/commit/2785ad72599ca5f9558a116baecd83a5bebe3292)) +- **ngValue:** set the element's value property in addition to the value attribute ([e6afca](https://github.com/angular/angular.js/commit/e6afca00c9061a3e13b570796ca3ab428c1723a1) [#14031](https://github.com/angular/angular.js/issues/14031)) +- **aria/ngModel:** do not overwrite the default `$isEmpty()` method for checkboxes ([975a61](https://github.com/angular/angular.js/commit/975a6170efceb2a5e6377c57329731c0636eb8c8) [#14621](https://github.com/angular/angular.js/issues/14621)) +- **$resource:** + - fulfill promise with the correct value on error ([5f6949](https://github.com/angular/angular.js/commit/5f6949fdae57b15340c1213cce379c6e6f8aff62) [#14837](https://github.com/angular/angular.js/issues/14837)) + - pass all extra, owned properties as params ([acb545](https://github.com/angular/angular.js/commit/acb545ec3ebf099db68561033645941c900973b5) [#14866](https://github.com/angular/angular.js/issues/14866)) + - add semicolon to whitelist of delimiters to unencode in URL params ([2456ab](https://github.com/angular/angular.js/commit/2456ab63a613902d21c151445f9c697a76ab43b3)) +- **$http:** + - avoid `Possibly Unhandled Rejection` error when the request fails ([47583d](https://github.com/angular/angular.js/commit/47583d98005f6a498d397dbe2cedaadac69f0b47) [#13869](https://github.com/angular/angular.js/issues/13869)) + - properly increment/decrement `$browser.outstandingRequestCount` ([4f6f2b](https://github.com/angular/angular.js/commit/4f6f2bce4ac93b85320e42e5023c09d099779b7d) [#13782](https://github.com/angular/angular.js/issues/13782) [#14921](https://github.com/angular/angular.js/issues/14921)) +- **ngMock:** trigger digest in `$httpBackend.verifyNoOutstandingRequest()` ([267ee9](https://github.com/angular/angular.js/commit/267ee9c892b0eb40908700ee2435793f8c6c1c84) [#13506](https://github.com/angular/angular.js/issues/13506)) +- **ngAria:** + - bind to `keydown` instead of `keypress` in `ngClick` ([ad41ba](https://github.com/angular/angular.js/commit/ad41baa1fdc057db3fe529ff87735b173b164b4c) [#14063](https://github.com/angular/angular.js/issues/14063)) + - don't add roles to native control elements ([9978de](https://github.com/angular/angular.js/commit/9978de11b7295fec1a2f4cb8fbeb9b62b54cb711) [#14076](https://github.com/angular/angular.js/issues/14076)) +- **ngBind:** use same string representation as `$interpolate` ([fa80a6](https://github.com/angular/angular.js/commit/fa80a61a05a3b49a2c770d5544cb8480907a18d3)) +- **ngMock/$httpBackend:** fail if a url is provided but is `undefined` ([7551b8](https://github.com/angular/angular.js/commit/7551b8975a91ee286cc2cf4af5e78f924533575e) [#8442](https://github.com/angular/angular.js/issues/8442) [#10934](https://github.com/angular/angular.js/issues/10934)) +- **$route:** don't process route change controllers and templates for `redirectTo` routes ([7f4b35](https://github.com/angular/angular.js/commit/7f4b356c2bebb87f0c26b57a20415b004b20bcd1) [#3332](https://github.com/angular/angular.js/issues/3332)) +- **loader:** `module.decorator` order of operations is now irrelevant ([6a2ebd](https://github.com/angular/angular.js/commit/6a2ebdba5df27e789e3cb10f11eedf90f7b9b97e) [#12382](https://github.com/angular/angular.js/issues/12382)) +- **ngAnimate:** make svg elements work with `classNameFilter` ([81bf7e](https://github.com/angular/angular.js/commit/81bf7ed73ee67f9eb997da869c52839449ca02b3)) + + +## New Features +- **jqLite:** + - implement `jqLite(f)` as alias to `jqLite(document).ready(f)` ([369fb7](https://github.com/angular/angular.js/commit/369fb7f4f73664bcdab0350701552d8bef6f605e)) + - don't throw for elements with missing `getAttribute` ([4e6c14](https://github.com/angular/angular.js/commit/4e6c14dcae4a9a30b3610a288ef8d20db47c4417)) + - don't remove a boolean attribute for `.attr(attrName, '')` ([3faf45](https://github.com/angular/angular.js/commit/3faf4505732758165083c9d21de71fa9b6983f4a)) + - remove the attribute for `.attr(attribute, null)` ([4e3624](https://github.com/angular/angular.js/commit/4e3624552284d0e725bf6262b2e468cd2c7682fa)) + - return `[]` for `.val()` on `` with no selection + +For the jqLite element representing a select element in +the multiple variant with no options chosen the .val() getter used to return +null and now returns an empty array. + +To migrate the code follow the example below: + +Before: + +HTML: + +``` + +``` + +JavaScript: + +``` + var value = $element.val(); + if (value) { + /* do something */ + } +``` + +After: + +HTML: + +``` + +``` + +JavaScript: + +``` + var value = $element.val(); + if (value.length > 0) { + /* do something */ + } +``` + + +### `ngModel` due to: + +- **[7bc71a](https://github.com/angular/angular.js/commit/7bc71adc63bb6bb609b44dd2d3ea8fb0cd3f300b)**: treat synchronous validators as boolean always + +Previously, only a literal `false` return would resolve as the +synchronous validator failing. Now, all traditionally false JavaScript values +are treated as failing the validator, as one would naturally expect. + +Specifically, the values `0` (the number zero), `null`, `NaN` and `''` (the +empty string) used to be considered valid (passing) and they are now considered +invalid (failing). The value `undefined` was treated similarly to a pending +asynchronous validator, causing the validation to be pending. `undefined` is +also now considered invalid. + +To migrate, make sure your synchronous validators are returning either a +literal `true` or a literal `false` value. For most code, we expect this to +already be the case. Only a very small subset of projects will be affected. + +Namely, anyone using `undefined` or any falsy value as a return will now see +their validation failing, whereas previously falsy values other than `undefined` +would have been seen as passing and `undefined` would have been seen as pending. + +- **[9e24e7](https://github.com/angular/angular.js/commit/9e24e774a558143b3478536911a3a4c1714564ba)**: change controllers to use prototype methods + +The use of prototype methods instead of new methods per instance removes the ability to pass +NgModelController and FormController methods without context. + +For example + +`$scope.$watch('something', myNgModelCtrl.$render)` + +will no longer work because the `$render` method is passed without any context. +This must now be replaced with + +``` +$scope.$watch('something', function() { + myNgModelCtrl.$render(); +}) +``` + +or possibly by using `Function.prototype.bind` or `angular.bind`. + + +- **[975a61](https://github.com/angular/angular.js/commit/975a6170efceb2a5e6377c57329731c0636eb8c8)**: do not overwrite the default `$isEmpty()` method for checkboxes + +Custom `checkbox`-shaped controls (e.g. checkboxes, menuitemcheckboxes), no longer have a custom +`$isEmpty()` method on their `NgModelController` that checks for `value === false`. Unless +overwritten, the default `$isEmpty()` method will be used, which treats `undefined`, `null`, `NaN` +and `''` as "empty". + +**Note:** The `$isEmpty()` method is used to determine if the checkbox is checked ("not empty" means + "checked") and thus it can indirectly affect other things, such as the control's validity + with respect to the `required` validator (e.g. "empty" + "required" --> "invalid"). + +Before: + +```js +var template = ''; +var customCheckbox = $compile(template)(scope); +var ctrl = customCheckbox.controller('ngModel'); + +scope.$apply('value = false'); +console.log(ctrl.$isEmpty()); //--> true + +scope.$apply('value = true'); +console.log(ctrl.$isEmpty()); //--> false + +scope.$apply('value = undefined'/* or null or NaN or '' */); +console.log(ctrl.$isEmpty()); //--> false +``` + +After: + +```js +var template = ''; +var customCheckbox = $compile(template)(scope); +var ctrl = customCheckbox.controller('ngModel'); + +scope.$apply('value = false'); +console.log(ctrl.$isEmpty()); //--> false + +scope.$apply('value = true'); +console.log(ctrl.$isEmpty()); //--> false + +scope.$apply('value = undefined'/* or null or NaN or '' */); +console.log(ctrl.$isEmpty()); //--> true +``` + +-- +If you want to have a custom `$isEmpty()` method, you need to overwrite the default. For example: + +```js +.directive('myCheckbox', function myCheckboxDirective() { + return { + require: 'ngModel', + link: function myCheckboxPostLink(scope, elem, attrs, ngModelCtrl) { + ngModelCtrl.$isEmpty = function myCheckboxIsEmpty(value) { + return !value; // Any falsy value means "empty" + + // Or to restore the previous behavior: + // return value === false; + }; + } + }; +}) +``` + +### `$http` due to: +- **[b54a39](https://github.com/angular/angular.js/commit/b54a39e2029005e0572fbd2ac0e8f6a4e5d69014)**: remove deprecated callback methods: `success()/error()` + +`$http`'s deprecated custom callback methods - `success()` and `error()` - have been removed. +You can use the standard `then()`/`catch()` promise methods instead, but note that the method +signatures and return values are different. + +`success(fn)` can be replaced with `then(fn)`, and `error(fn)` can be replaced with either +`then(null, fn)` or `catch(fn)`. + +Before: + +```js +$http(...). + success(function onSuccess(data, status, headers, config) { + // Handle success + ... + }). + error(function onError(data, status, headers, config) { + // Handle error + ... + }); +``` + +After: + +```js +$http(...). + then(function onSuccess(response) { + // Handle success + var data = response.data; + var status = response.status; + var statusText = response.statusText; + var headers = response.headers; + var config = response.config; + ... + }, function onError(response) { + // Handle error + var data = response.data; + var status = response.status; + var statusText = response.statusText; + var headers = response.headers; + var config = response.config; + ... + }); + +// or + +$http(...). + then(function onSuccess(response) { + // Handle success + var data = response.data; + var status = response.status; + var statusText = response.statusText; + var headers = response.headers; + var config = response.config; + ... + }). + catch(function onError(response) { + // Handle error + var data = response.data; + var status = response.status; + var statusText = response.statusText; + var headers = response.headers; + var config = response.config; + ... + }); +``` + +**Note:** +There is a subtle difference between the variations showed above. When using +`$http(...).success(onSuccess).error(onError)` or `$http(...).then(onSuccess, onError)`, the +`onError()` callback will only handle errors/rejections produced by the `$http()` call. If the +`onSuccess()` callback produces an error/rejection, it won't be handled by `onError()` and might go +unnoticed. In contrast, when using `$http(...).then(onSuccess).catch(onError)`, `onError()` will +handle errors/rejections produced by both `$http()` _and_ `onSuccess()`. + +- **[fb6634](https://github.com/angular/angular.js/commit/fb663418710736161a6b5da49c345e92edf58dcb)**: JSONP callback must be specified by `jsonpCallbackParam` config + +You can no longer use the `JSON_CALLBACK` placeholder in your JSONP requests. +Instead you must provide the name of the query parameter that will pass the +callback via the `jsonpCallbackParam` property of the config object, or app-wide via +the `$http.defaults.jsonpCallbackParam` property, which is `"callback"` by default. + +Before this change: + +``` +$http.json('trusted/url?callback=JSON_CALLBACK'); +$http.json('other/trusted/url', {params: {cb:'JSON_CALLBACK'}}); +``` + +After this change: + +``` +$http.json('trusted/url'); +$http.json('other/trusted/url', {jsonpCallbackParam:'cb'}); +``` + +- **[6476af](https://github.com/angular/angular.js/commit/6476af83cd0418c84e034a955b12a842794385c4)**: JSONP requests now require a trusted resource URL + +All JSONP requests now require the URL to be trusted as resource URLs. +There are two approaches to trust a URL: + +**Whitelisting with the `$sceDelegateProvider.resourceUrlWhitelist()` +method.** + +You configure this list in a module configuration block: + +``` +appModule.config(['$sceDelegateProvider', function($sceDelegateProvider) { + $sceDelegateProvider.resourceUrlWhiteList([ + // Allow same origin resource loads. + 'self', + // Allow JSONP calls that match this pattern + 'https://some.dataserver.com/**.jsonp?**` + ]); +}]); +``` + +**Explicitly trusting the URL via the `$sce.trustAsResourceUrl(url)` +method** + +You can pass a trusted object instead of a string as a URL to the `$http` +service: + +``` +var promise = $http.jsonp($sce.trustAsResourceUrl(url)); +``` + +- **[4f6f2b](https://github.com/angular/angular.js/commit/4f6f2bce4ac93b85320e42e5023c09d099779b7d)**: properly increment/decrement `$browser.outstandingRequestCount` + +HTTP requests now update the outstanding request count synchronously. +Previously the request count would not have been updated until the +request to the server is actually in flight. Now the request count is +updated before the async interceptor is called. + +The new behaviour is correct but it may change the expected behaviour in +a small number of e2e test cases where an async request interceptor is +being used. + + +### `$q` due to: + +- **[e13eea](https://github.com/angular/angular.js/commit/e13eeabd7e34a78becec06cfbe72c23f2dcb85f9)**: treat thrown errors as regular rejections + +Previously, throwing an error from a promise's `onFulfilled` or `onRejection` handlers, would result +in passing the error to the `$exceptionHandler()` (in addition to rejecting the promise with the +error as reason). + +Now, a thrown error is treated exactly the same as a regular rejection. This applies to all +services/controllers/filters etc that rely on `$q` (including built-in services, such as `$http` and +`$route`). For example, `$http`'s `transformRequest/Response` functions or a route's `redirectTo` +function as well as functions specified in a route's `resolve` object, will no longer result in a +call to `$exceptionHandler()` if they throw an error. Other than that, everything will continue to +behave in the same way; i.e. the promises will be rejected, route transition will be cancelled, +`$routeChangeError` events will be broadcasted etc. + +- **[c9dffd](https://github.com/angular/angular.js/commit/c9dffde1cb167660120753181cb6d01dc1d1b3d0)**: report promises with non rejection callback + +Unhandled rejected promises will be logged to $exceptionHandler. + +Tests that depend on specific order or number of messages in $exceptionHandler +will need to handle rejected promises report. + + +### `ngTransclude` due to: + +- **[32aa7e](https://github.com/angular/angular.js/commit/32aa7e7395527624119e3917c54ee43b4d219301)**: use fallback content if only whitespace is provided + +Previously whitespace only transclusion would be treated as the transclusion +being "not empty", which meant that fallback content was not used in that +case. + +Now if you only provide whitespace as the transclusion content, it will be +assumed to be empty and the fallback content will be used instead. + +If you really do want whitespace then you can force it to be used by adding +a comment to the whitespace. + + +### `ngModelOptions` due to: + +- **[87a2ff](https://github.com/angular/angular.js/commit/87a2ff76af5d0a9268d8eb84db5755077d27c84c)**: allow options to be inherited from ancestor `ngModelOptions` + +Previously, if a setting was not applied on `ngModelOptions`, then it would default +to undefined. Now the setting will be inherited from the nearest ngModelOptions +ancestor. + +It is possible that an `ngModelOptions` directive that does not set a property, +has an ancestor ngModelOptions that does set this property to a value other than +`undefined`. This would cause the `ngModel` and input controls below this `ngModelOptions` +directive to display different behaviour. This is fixed by explictly setting the +property in the `ngModelOptions` to prevent it from inheriting from the ancestor. + +For example if you had the following HTML: + +``` +
+ +
+``` + +Then before this change the input would update on the default event not blur. +After this change the input will inherit the option to update on blur. +If you want the original behaviour then you will need to specify the option +on the input as well: + +``` +
+ +
+``` + +The programmatic API for `ngModelOptions` has changed. You must now read options +via the `getOption` method, rather than accessing the option directly as a property +of the options object. This does not affect the usage in templates and only +affects custom directives that might have been reading options for their own purposes. + + +### `$compile` due to: + +- **[13c252](https://github.com/angular/angular.js/commit/13c2522baf7c8f616b2efcaab4bffd54c8736591)**: correctly merge consecutive text nodes on IE11 + +**Note:** Everything described below affects **IE11 only**. + +Previously, consecutive text nodes would not get merged if they had no parent. They will now, which +might have unexpectd side effects in the following cases: + +1. Passing an array or jqLite/jQuery collection of parent-less text nodes to `$compile` directly: + + ```js + // Assuming: + var textNodes = [ + document.createTextNode('{{'), + document.createTextNode('"foo:"'), + document.createTextNode('}}') + ]; + var compiledNodes = $compile(textNodes)($rootScope); + + // Before: + console.log(compiledNodes.length); // 3 + console.log(compiledNodes.text()); // {{'foo'}} + + // After: + console.log(compiledNodes.length); // 1 + console.log(compiledNodes.text()); // foo + + // To get the old behavior, compile each node separately: + var textNodes = [ + document.createTextNode('{{'), + document.createTextNode('"foo"'), + document.createTextNode('}}') + ]; + var compiledNodes = angular.element(textNodes.map(function (node) { + return $compile(node)($rootScope)[0]; + })); + ``` + +2. Using multi-slot transclusion with non-consecutive, default-content text nodes (that form + interpolated expressions when merged): + + ```js + // Assuming the following component: + .compoent('someThing', { + template: '' + transclude: { + ignored: 'veryImportantContent' + } + }) + ``` + + ```html + + + {{ + Nooot + 'foo'}} + + + + + + {{ <-- Two separate + 'foo'}} <-- text nodes + + + + + + + foo <-- The text nodes were merged into `{{'foo'}}`, which was then interpolated + + + + + + + {{ + Nooot + 'foo'}} + + + + + + {{ <-- Two separate + 'foo'}} <-- nodes + + + ``` + +- **[b89c21](https://github.com/angular/angular.js/commit/b89c2181a9a165e06c027390164e08635ec449f4)**: move check for interpolation of on-event attributes to compile time + +Using interpolation in any on* event attributes (e.g. `