From 0d79005f8d1f4d674bb04ba93c41bb9c06280b4f Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Tue, 24 Jan 2017 10:46:28 -0500 Subject: [PATCH] fix(angular): add compatibility with Angular 1.6 - Change how ngModelOptions is used to conform to Angular 1.6 - Catch rejections as per change to $q Closes #6427 Fixes #6360 --- Gruntfile.js | 2 +- package.json | 6 +-- src/accordion/test/accordion.spec.js | 4 +- src/alert/test/alert.spec.js | 2 +- src/collapse/collapse.js | 4 +- src/datepicker/datepicker.js | 60 +++++++++++++++++++++------- src/datepickerPopup/popup.js | 40 ++++++++++++++----- src/modal/modal.js | 8 ++-- src/modal/test/modal.spec.js | 11 +++-- src/typeahead/typeahead.js | 30 +++++++++++--- 10 files changed, 119 insertions(+), 48 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 3d7f3d50ec..ed0ec0576a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,7 +9,7 @@ module.exports = function(grunt) { grunt.util.linefeed = '\n'; grunt.initConfig({ - ngversion: '1.5.8', + ngversion: '1.6.1', bsversion: '3.3.7', modules: [],//to be filled in by build task pkg: grunt.file.readJSON('package.json'), diff --git a/package.json b/package.json index d3794b1b7c..351c21cefa 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,9 @@ "url": "https://github.com/angular-ui/bootstrap.git" }, "devDependencies": { - "angular": "1.5.8", - "angular-mocks": "1.5.8", - "angular-sanitize": "1.5.8", + "angular": "1.6.1", + "angular-mocks": "1.6.1", + "angular-sanitize": "1.6.1", "grunt": "^0.4.5", "grunt-cli": "^1.2.0", "grunt-contrib-concat": "^1.0.0", diff --git a/src/accordion/test/accordion.spec.js b/src/accordion/test/accordion.spec.js index 1879d6f9c4..bb64a4b2c6 100644 --- a/src/accordion/test/accordion.spec.js +++ b/src/accordion/test/accordion.spec.js @@ -551,7 +551,7 @@ describe('uib-accordion', function() { }); it('attaches the same scope to the transcluded heading and body', function() { - expect(findGroupLink(0).find('span.ng-scope').scope().$id).toBe(findGroupBody(0).find('span').scope().$id); + expect(findGroupLink(0).scope().$id).toBe(findGroupBody(0).scope().$id); }); it('should wrap the transcluded content in a span', function() { @@ -580,7 +580,7 @@ describe('uib-accordion', function() { }); it('attaches the same scope to the transcluded heading and body', function() { - expect(findGroupLink(0).find('span.ng-scope').scope().$id).toBe(findGroupBody(0).find('span').scope().$id); + expect(findGroupLink(0).scope().$id).toBe(findGroupBody(0).scope().$id); }); it('should have disabled styling when is-disabled is true', isDisabledStyleCheck); diff --git a/src/alert/test/alert.spec.js b/src/alert/test/alert.spec.js index d38b4fab60..bc87c7f7ad 100644 --- a/src/alert/test/alert.spec.js +++ b/src/alert/test/alert.spec.js @@ -36,7 +36,7 @@ describe('uib-alert', function() { } function findContent(index) { - return element.find('div[ng-transclude] span').eq(index); + return element.find('div[ng-transclude]').eq(index); } it('should expose the controller to the view', function() { diff --git a/src/collapse/collapse.js b/src/collapse/collapse.js index d809a1df91..7e605b641d 100644 --- a/src/collapse/collapse.js +++ b/src/collapse/collapse.js @@ -72,7 +72,7 @@ angular.module('ui.bootstrap.collapse', []) to: getScrollFromElement(element[0]) }).then(expandDone); } - }); + }, angular.noop); } function expandDone() { @@ -111,7 +111,7 @@ angular.module('ui.bootstrap.collapse', []) to: cssTo }).then(collapseDone); } - }); + }, angular.noop); } function collapseDone() { diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index ca21a459b7..fd4e263660 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -104,7 +104,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst $scope.$watch('datepickerOptions.' + key, function(value) { if (value) { if (angular.isDate(value)) { - self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone); + self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.getOption('timezone')); } else { if ($datepickerLiteralWarning) { $log.warn('Literal date support has been deprecated, please switch to date object usage'); @@ -114,7 +114,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst } } else { self[key] = datepickerConfig[key] ? - dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : + dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.getOption('timezone')) : null; } @@ -161,14 +161,13 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; - ngModelOptions = ngModelCtrl_.$options || - $scope.datepickerOptions.ngModelOptions || - datepickerConfig.ngModelOptions; + ngModelOptions = extractOptions(ngModelCtrl); + if ($scope.datepickerOptions.initDate) { - self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.timezone) || new Date(); + self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.getOption('timezone')) || new Date(); $scope.$watch('datepickerOptions.initDate', function(initDate) { if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) { - self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone); + self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.getOption('timezone')); self.refreshView(); } }); @@ -178,8 +177,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date(); this.activeDate = !isNaN(date) ? - dateParser.fromTimezone(date, ngModelOptions.timezone) : - dateParser.fromTimezone(new Date(), ngModelOptions.timezone); + dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')) : + dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); ngModelCtrl.$render = function() { self.render(); @@ -192,7 +191,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst isValid = !isNaN(date); if (isValid) { - this.activeDate = dateParser.fromTimezone(date, ngModelOptions.timezone); + this.activeDate = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); } else if (!$datepickerSuppressError) { $log.error('Datepicker directive: "ng-model" value must be a Date object'); } @@ -209,7 +208,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst } var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; - date = dateParser.fromTimezone(date, ngModelOptions.timezone); + date = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); ngModelCtrl.$setValidity('dateDisabled', !date || this.element && !this.isDisabled(date)); } @@ -217,9 +216,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst this.createDateObject = function(date, format) { var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; - model = dateParser.fromTimezone(model, ngModelOptions.timezone); + model = dateParser.fromTimezone(model, ngModelOptions.getOption('timezone')); var today = new Date(); - today = dateParser.fromTimezone(today, ngModelOptions.timezone); + today = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); var time = this.compare(date, today); var dt = { date: date, @@ -265,9 +264,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst $scope.select = function(date) { if ($scope.datepickerMode === self.minMode) { - var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.timezone) : new Date(0, 0, 0, 0, 0, 0, 0); + var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.getOption('timezone')) : new Date(0, 0, 0, 0, 0, 0, 0); dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); - dt = dateParser.toTimezone(dt, ngModelOptions.timezone); + dt = dateParser.toTimezone(dt, ngModelOptions.getOption('timezone')); ngModelCtrl.$setViewValue(dt); ngModelCtrl.$render(); } else { @@ -352,6 +351,37 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst $scope.datepickerMode = mode; $scope.datepickerOptions.datepickerMode = mode; } + + function extractOptions(ngModelCtrl) { + var ngModelOptions; + + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = ngModelCtrl.$options || + $scope.datepickerOptions.ngModelOptions || + datepickerConfig.ngModelOptions || + {}; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + // ng-model-options defaults timezone to null; don't let its precedence squash a non-null value + var timezone = ngModelCtrl.$options.getOption('timezone') || + ($scope.datepickerOptions.ngModelOptions ? $scope.datepickerOptions.ngModelOptions.timezone : null) || + (datepickerConfig.ngModelOptions ? datepickerConfig.ngModelOptions.timezone : null); + + // values passed to createChild override existing values + ngModelOptions = ngModelCtrl.$options // start with a ModelOptions instance + .createChild(datepickerConfig.ngModelOptions) // lowest precedence + .createChild($scope.datepickerOptions.ngModelOptions) + .createChild(ngModelCtrl.$options) // highest precedence + .createChild({timezone: timezone}); // to keep from squashing a non-null value + } + + return ngModelOptions; + } }]) .controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { diff --git a/src/datepickerPopup/popup.js b/src/datepickerPopup/popup.js index 0d03c4e73d..02c0e88a1c 100644 --- a/src/datepickerPopup/popup.js +++ b/src/datepickerPopup/popup.js @@ -32,11 +32,7 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ this.init = function(_ngModel_) { ngModel = _ngModel_; - ngModelOptions = angular.isObject(_ngModel_.$options) ? - _ngModel_.$options : - { - timezone: null - }; + ngModelOptions = extractOptions(ngModel); closeOnDateSelection = angular.isDefined($attrs.closeOnDateSelection) ? $scope.$parent.$eval($attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection; @@ -127,13 +123,13 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ value = new Date(value); } - $scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone); + $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); return dateParser.filter($scope.date, dateFormat); }); } else { ngModel.$formatters.push(function(value) { - $scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone); + $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); return value; }); } @@ -185,7 +181,7 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ $scope.isDisabled = function(date) { if (date === 'today') { - date = dateParser.fromTimezone(new Date(), ngModelOptions.timezone); + date = dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); } var dates = {}; @@ -242,7 +238,7 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ date = new Date($scope.date); date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); } else { - date = dateParser.fromTimezone(today, ngModelOptions.timezone); + date = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); date.setHours(0, 0, 0, 0); } } @@ -333,11 +329,11 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ if (angular.isString(viewValue)) { var date = parseDateString(viewValue); if (!isNaN(date)) { - return dateParser.toTimezone(date, ngModelOptions.timezone); + return dateParser.toTimezone(date, ngModelOptions.getOption('timezone')); } } - return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined; + return ngModelOptions.getOption('allowInvalid') ? viewValue : undefined; } function validator(modelValue, viewValue) { @@ -412,6 +408,28 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ } } + function extractOptions(ngModelCtrl) { + var ngModelOptions; + + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = angular.isObject(ngModelCtrl.$options) ? + ngModelCtrl.$options : + { + timezone: null + }; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + ngModelOptions = ngModelCtrl.$options; + } + + return ngModelOptions; + } + $scope.$on('uib:datepicker.mode', function() { $timeout(positionPopup, 0, false); }); diff --git a/src/modal/modal.js b/src/modal/modal.js index eeb79c101e..679ff188e8 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -419,10 +419,6 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.sta var appendToElement = modal.appendTo, currBackdropIndex = backdropIndex(); - if (!appendToElement.length) { - throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); - } - if (currBackdropIndex >= 0 && !backdropDomEl) { backdropScope = $rootScope.$new(true); backdropScope.modalOptions = modal; @@ -699,6 +695,10 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.sta modalOptions.resolve = modalOptions.resolve || {}; modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0); + if (!modalOptions.appendTo.length) { + throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); + } + //verify options if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) { throw new Error('One of component or template or templateUrl options is required.'); diff --git a/src/modal/test/modal.spec.js b/src/modal/test/modal.spec.js index 86f0a107c7..4bf85243da 100644 --- a/src/modal/test/modal.spec.js +++ b/src/modal/test/modal.spec.js @@ -296,6 +296,8 @@ describe('$uibModal', function() { function open(modalOptions, noFlush, noDigest) { var modal = $uibModal.open(modalOptions); + modal.opened['catch'](angular.noop); + modal.result['catch'](angular.noop); if (!noDigest) { $rootScope.$digest(); @@ -1612,7 +1614,7 @@ describe('$uibModal', function() { var windowEl = $compile('
content
')($rootScope); $rootScope.$digest(); - expect(windowEl.html()).toBe('
content
'); + expect(windowEl.html()).toBe('
content
'); })); }); @@ -1736,16 +1738,19 @@ describe('$uibModal', function() { ds[x] = {index: i, deferred: $q.defer(), reject: reject}; var scope = $rootScope.$new(); + var failed = false; scope.index = i; open({ template: '
' + i + '
', scope: scope, resolve: { - x: function() { return ds[x].deferred.promise; } + x: function() { return ds[x].deferred.promise['catch'](function () { + failed = true; + }); } } }, true).opened.then(function() { expect($uibModalStack.getTop().value.modalScope.index).toEqual(i); - actual += i; + if (!failed) { actual += i; } }); }); diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 81492f03dc..71b807c817 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -94,7 +94,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap var invokeModelSetter = $parse(attrs.ngModel + '($$$p)'); var $setModelValue = function(scope, newValue) { if (angular.isFunction(parsedModel(originalScope)) && - ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) { + ngModelOptions.getOption('getterSetter')) { return invokeModelSetter(scope, {$$$p: newValue}); } @@ -507,11 +507,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap element.after($popup); } - this.init = function(_modelCtrl, _ngModelOptions) { + this.init = function(_modelCtrl) { modelCtrl = _modelCtrl; - ngModelOptions = _ngModelOptions; + ngModelOptions = extractOptions(modelCtrl); - scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope); + scope.debounceUpdate = $parse(ngModelOptions.getOption('debounce'))(originalScope); //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue @@ -571,14 +571,32 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; }); }; + + function extractOptions(ngModelCtrl) { + var ngModelOptions; + + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = ngModelCtrl.$options || {}; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + ngModelOptions = ngModelCtrl.$options; + } + + return ngModelOptions; + } }]) .directive('uibTypeahead', function() { return { controller: 'UibTypeaheadController', - require: ['ngModel', '^?ngModelOptions', 'uibTypeahead'], + require: ['ngModel', 'uibTypeahead'], link: function(originalScope, element, attrs, ctrls) { - ctrls[2].init(ctrls[0], ctrls[1]); + ctrls[1].init(ctrls[0]); } }; })