Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.

Commit c4171a2

Browse files
deegwesleycho
authored andcommitted
chore(datepicker): unregister parent watchers on $destroy
Closes #5242 Closes #5260
1 parent a5797b9 commit c4171a2

File tree

2 files changed

+84
-18
lines changed

2 files changed

+84
-18
lines changed

Diff for: src/datepicker/datepicker.js

+29-17
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
2626
function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError, dateParser) {
2727
var self = this,
2828
ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl;
29-
ngModelOptions = {};
29+
ngModelOptions = {},
30+
watchListeners = [];
3031

3132
// Modes chain
3233
this.modes = ['day', 'month', 'year'];
@@ -44,24 +45,24 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
4445
// Watchable date attributes
4546
angular.forEach(['minDate', 'maxDate'], function(key) {
4647
if ($attrs[key]) {
47-
$scope.$parent.$watch($attrs[key], function(value) {
48+
watchListeners.push($scope.$parent.$watch($attrs[key], function(value) {
4849
self[key] = value ? angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium')) : null;
4950
self.refreshView();
50-
});
51+
}));
5152
} else {
5253
self[key] = datepickerConfig[key] ? dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : null;
5354
}
5455
});
5556

5657
angular.forEach(['minMode', 'maxMode'], function(key) {
5758
if ($attrs[key]) {
58-
$scope.$parent.$watch($attrs[key], function(value) {
59+
watchListeners.push($scope.$parent.$watch($attrs[key], function(value) {
5960
self[key] = $scope[key] = angular.isDefined(value) ? value : $attrs[key];
6061
if (key === 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key]) ||
6162
key === 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key])) {
6263
$scope.datepickerMode = self[key];
6364
}
64-
});
65+
}));
6566
} else {
6667
self[key] = $scope[key] = datepickerConfig[key] || null;
6768
}
@@ -72,22 +73,22 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
7273

7374
if (angular.isDefined($attrs.initDate)) {
7475
this.activeDate = dateParser.fromTimezone($scope.$parent.$eval($attrs.initDate), ngModelOptions.timezone) || new Date();
75-
$scope.$parent.$watch($attrs.initDate, function(initDate) {
76+
watchListeners.push($scope.$parent.$watch($attrs.initDate, function(initDate) {
7677
if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
7778
self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone);
7879
self.refreshView();
7980
}
80-
});
81+
}));
8182
} else {
8283
this.activeDate = new Date();
8384
}
8485

8586
$scope.disabled = angular.isDefined($attrs.disabled) || false;
8687
if (angular.isDefined($attrs.ngDisabled)) {
87-
$scope.$parent.$watch($attrs.ngDisabled, function(disabled) {
88+
watchListeners.push($scope.$parent.$watch($attrs.ngDisabled, function(disabled) {
8889
$scope.disabled = disabled;
8990
self.refreshView();
90-
});
91+
}));
9192
}
9293

9394
$scope.isActive = function(dateObject) {
@@ -248,6 +249,13 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
248249
self.refreshView();
249250
}
250251
};
252+
253+
$scope.$on("$destroy", function() {
254+
//Clear all watch listeners on destroy
255+
while (watchListeners.length) {
256+
watchListeners.shift()();
257+
}
258+
});
251259
}])
252260

253261
.controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
@@ -573,12 +581,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
573581

574582
.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig',
575583
function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig) {
576-
var self = this;
577584
var cache = {},
578585
isHtml5DateInput = false;
579586
var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus,
580587
datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl,
581-
ngModel, ngModelOptions, $popup, altInputFormats;
588+
ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = [];
582589

583590
scope.watchData = {};
584591

@@ -683,7 +690,7 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
683690
if (attrs[key]) {
684691
var getAttribute = $parse(attrs[key]);
685692

686-
scope.$parent.$watch(getAttribute, function(value) {
693+
watchListeners.push(scope.$parent.$watch(getAttribute, function(value) {
687694
if (key === 'minDate' || key === 'maxDate') {
688695
if (value === null) {
689696
cache[key] = null;
@@ -697,7 +704,7 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
697704
} else {
698705
scope.watchData[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone);
699706
}
700-
});
707+
}));
701708

702709
datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
703710
}
@@ -729,7 +736,7 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
729736
}
730737
scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone);
731738
dateFormat = dateFormat.replace(/M!/, 'MM')
732-
.replace(/d!/, 'dd');
739+
.replace(/d!/, 'dd');
733740

734741
return dateFilter(scope.date, dateFormat);
735742
});
@@ -769,6 +776,11 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
769776
$popup.remove();
770777
element.unbind('keydown', inputKeydownBind);
771778
$document.unbind('click', documentClickBind);
779+
780+
//Clear all watch listeners on destroy
781+
while (watchListeners.length) {
782+
watchListeners.shift()();
783+
}
772784
});
773785
};
774786

@@ -782,7 +794,7 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
782794
}
783795

784796
return scope.watchData.minDate && scope.compare(date, cache.minDate) < 0 ||
785-
scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0;
797+
scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0;
786798
};
787799

788800
scope.compare = function(date1, date2) {
@@ -832,9 +844,9 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
832844

833845
scope.disabled = angular.isDefined(attrs.disabled) || false;
834846
if (attrs.ngDisabled) {
835-
scope.$parent.$watch($parse(attrs.ngDisabled), function(disabled) {
847+
watchListeners.push(scope.$parent.$watch($parse(attrs.ngDisabled), function(disabled) {
836848
scope.disabled = disabled;
837-
});
849+
}));
838850
}
839851

840852
scope.$watch('isOpen', function(value) {

Diff for: src/datepicker/test/datepicker.spec.js

+55-1
Original file line numberDiff line numberDiff line change
@@ -994,7 +994,6 @@ describe('datepicker', function() {
994994
expect(angular.element(button).prop('disabled')).toBe(false);
995995
});
996996
});
997-
998997
});
999998

1000999
describe('`min-date` attribute', function () {
@@ -1203,6 +1202,38 @@ describe('datepicker', function() {
12031202
});
12041203
});
12051204

1205+
describe('gc', function() {
1206+
var datepickerScope;
1207+
beforeEach(function() {
1208+
$rootScope.minDate = new Date();
1209+
$rootScope.maxDate = new Date();
1210+
$rootScope.maxDate.setDate($rootScope.maxDate.getDate() + 1);
1211+
$rootScope.minMode = 'day';
1212+
$rootScope.maxMode = 'year';
1213+
$rootScope.initDate = new Date();
1214+
element = $compile('<uib-datepicker ng-model="date" min-date="minDate" max-date="maxDate" min-mode="minMode" max-mode="maxMode" init-date="initDate"></uib-datepicker>')($rootScope);
1215+
$rootScope.$digest();
1216+
datepickerScope = element.isolateScope();
1217+
});
1218+
1219+
it('should appropriately clean up $watch expressions', function() {
1220+
expect($rootScope.$$watchers.length).toBe(6);
1221+
['minDate', 'maxDate', 'minMode', 'maxMode', 'initDate'].forEach(function(prop) {
1222+
var $$watcher;
1223+
$rootScope.$$watchers.forEach(function($$watch) {
1224+
if ($$watch.exp === prop) {
1225+
$$watcher = $$watch;
1226+
}
1227+
});
1228+
expect(angular.isObject($$watcher)).toBe(true);
1229+
});
1230+
1231+
datepickerScope.$destroy();
1232+
1233+
expect($rootScope.$$watchers.length).toBe(1);
1234+
});
1235+
});
1236+
12061237
describe('setting datepickerConfig', function() {
12071238
var originalConfig = {};
12081239
beforeEach(inject(function(uibDatepickerConfig) {
@@ -2820,6 +2851,29 @@ describe('datepicker', function() {
28202851
});
28212852
});
28222853

2854+
describe('gc', function() {
2855+
var popupScope;
2856+
beforeEach(function() {
2857+
$rootScope.minDate = new Date();
2858+
$rootScope.maxDate = new Date();
2859+
$rootScope.maxDate.setDate($rootScope.maxDate.getDate() + 1);
2860+
$rootScope.minMode = 'day';
2861+
$rootScope.maxMode = 'year';
2862+
$rootScope.initDate = new Date();
2863+
element = $compile('<div><input ng-model="date" uib-datepicker-popup min-date="minDate" max-date="maxDate" min-mode="minMode" max-mode="maxMode" init-date="initDate"></uib-datepicker>')($rootScope);
2864+
$rootScope.$digest();
2865+
popupScope = element.find('input').isolateScope();
2866+
});
2867+
2868+
it('should appropriately clean up $watch expressions', function() {
2869+
expect($rootScope.$$watchers.length).toBe(4);
2870+
2871+
popupScope.$destroy();
2872+
2873+
expect($rootScope.$$watchers.length).toBe(1);
2874+
});
2875+
});
2876+
28232877
describe('with empty initial state', function() {
28242878
beforeEach(inject(function() {
28252879
$rootScope.date = null;

0 commit comments

Comments
 (0)