Permalink
Browse files

fix(datepicker): remove use of ng-class to reduce watchers

Now use at most one watcher for determining which date should have what
class. ng-class adds a watcher per element containing date, while this
uses one watcher per class for all elements.
  • Loading branch information...
chrisirhc committed Jun 8, 2015
1 parent 2113aec commit a14c6dd071677b0844d25fa0997333c007e887fe
@@ -59,14 +59,6 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
this.activeDate = new Date();
}

$scope.isActive = function(dateObject) {
if (self.compare(dateObject.date, self.activeDate) === 0) {
$scope.activeDateId = dateObject.uid;
return true;
}
return false;
};

this.init = function( ngModelCtrl_ ) {
ngModelCtrl = ngModelCtrl_;

@@ -92,7 +84,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst

this.refreshView = function() {
if ( this.element ) {
$scope.selectedDt = null;
this._refreshView();
if ($scope.activeDt) {
$scope.activeDateId = $scope.activeDt.uid;
}

var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date)));
@@ -101,14 +97,21 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst

this.createDateObject = function(date, format) {
var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
return {
var dt = {
date: date,
label: dateFilter(date, format),
selected: model && this.compare(date, model) === 0,
disabled: this.isDisabled(date) || false,
current: this.compare(date, new Date()) === 0,
customClass: this.customClass(date) || null
};

if (model && this.compare(date, model) === 0) {
$scope.selectedDt = dt;
}
if (self.activeDate && this.compare(dt.date, self.activeDate) === 0) {
$scope.activeDt = dt;
}
return dt;
};

this.isDisabled = function( date ) {
@@ -728,6 +731,104 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
};
}])

// Avoiding use of ng-class as it creates a lot of watchers when a class is to be applied to
// at most one element.
.directive('datepickerIsClass', [
'$animate',
function ($animate) {
// 11111111 22222222
var ON_REGEXP = /^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/;
// 11111111 22222222
var IS_REGEXP = /^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/;

var dataPerTracked = {};

return {
restrict: 'A',
compile: function (tElement, tAttrs) {
var linkedScopes = [];
var instances = [];
var expToData = {};
var lastActivated = null;
var onExpMatches = tAttrs.datepickerIsClass.match(ON_REGEXP);
var onExp = onExpMatches[2];
var expsStr = onExpMatches[1];
var exps = expsStr.split(',');

return linkFn;

function linkFn(scope, element, attrs) {
linkedScopes.push(scope);
instances.push({
scope: scope,
element: element
});

exps.forEach(function (exp, k) {
addForExp(exp, scope);
});

scope.$on('$destroy', removeScope);
}

function addForExp(exp, scope) {
var matches = exp.match(IS_REGEXP);
var clazz = scope.$eval(matches[1]);
var compareWithExp = matches[2];
var data = expToData[exp];
if (!data) {
var watchFn = function (compareWithVal) {
var newActivated = null;
instances.some(function (instance) {
var thisVal = instance.scope.$eval(onExp);
if (thisVal === compareWithVal) {
newActivated = instance;
return true;
}
});
if (data.lastActivated !== newActivated) {
if (data.lastActivated) {
$animate.removeClass(data.lastActivated.element, clazz);
}
if (newActivated) {
$animate.addClass(newActivated.element, clazz);
}
data.lastActivated = newActivated;
}
};
expToData[exp] = data = {
lastActivated: null,
scope: scope,
watchFn: watchFn,
compareWithExp: compareWithExp,
watcher: scope.$watch(compareWithExp, watchFn)
};
}
data.watchFn(scope.$eval(compareWithExp));
}

function removeScope(e) {
var removedScope = e.targetScope;
var index = linkedScopes.indexOf(removedScope);
linkedScopes.splice(index, 1);
instances.splice(index, 1);
if (linkedScopes.length) {
var newWatchScope = linkedScopes[0];
angular.forEach(expToData, function (data) {
if (data.scope === removedScope) {
data.watcher = newWatchScope.$watch(data.compareWithExp, data.watchFn);
data.scope = newWatchScope;
}
});
}
else {
expToData = {};
}
}
}
};
}])

.directive('datepickerPopupWrap', function() {
return {
restrict:'EA',
@@ -2184,4 +2184,83 @@ describe('datepicker directive', function () {
});
});
});

describe('datepickerIsClass', function () {

beforeEach(function() {
$rootScope.activeClass = 'active';
$rootScope.items = [1, 2, 3];
element = $compile('<div><div ng-repeat="item in items" ' +
'datepicker-is-class="activeClass for activeItem on item">{{ item }}</div></div>')($rootScope);
$rootScope.$digest();
});

it('initializes classes correctly', function () {
expect(element.find('.active').length).toEqual(0);
});

it('sets classes correctly', function () {
$rootScope.activeItem = 2;
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('2');

$rootScope.items.splice(1, 1);
$rootScope.$digest();
expect(element.find('.active').length).toEqual(0);
});

it('handles removal of items correctly', function () {
$rootScope.activeItem = 2;
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('2');

$rootScope.items.splice(1, 1);
$rootScope.$digest();
expect(element.find('.active').length).toEqual(0);

$rootScope.activeItem = 1;
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('1');
});

it('handles moving of items', function () {
$rootScope.activeItem = 2;
$rootScope.items = [2, 1, 3];
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('2');
expect(element.find('.active').length).toEqual(1);
expect(element.find('.active').index()).toEqual(0);

$rootScope.items = [4, 3, 2];
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('2');
expect(element.find('.active').length).toEqual(1);
expect(element.find('.active').index()).toEqual(2);
});

it('handles emptying and re-adding the items', function () {
$rootScope.activeItem = 2;
$rootScope.items = [];
$rootScope.$digest();
expect(element.find('.active').length).toEqual(0);

$rootScope.items = [4, 3, 2];
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('2');
expect(element.find('.active').index()).toEqual(2);
});

it('handles undefined items', function () {
$rootScope.activeItem = undefined;
$rootScope.items = [];
$rootScope.$digest();
expect(element.find('.active').length).toEqual(0);

$rootScope.items = [4, 3, undefined];
$rootScope.$digest();
expect(element.find('.active').length).toEqual(1);
expect(element.find('.active').text()).toEqual('');
});

});
});
@@ -17,7 +17,10 @@
id="{{::dt.uid}}"
ng-class="::dt.customClass">
<button type="button" style="width:100%;" class="btn btn-default btn-sm"
ng-class="{'btn-info': dt.selected, active: isActive(dt)}"
datepicker-is-class="
'btn-info' for selectedDt,
'active' for activeDt
on dt"
ng-click="select(dt.date)"
ng-disabled="::dt.disabled"
tabindex="-1"><span
@@ -10,7 +10,10 @@
<tr ng-repeat="row in rows track by $index">
<td ng-repeat="dt in row" class="text-center" role="gridcell" id="{{::dt.uid}}">
<button type="button" style="width:100%;" class="btn btn-default"
ng-class="{'btn-info': dt.selected, active: isActive(dt)}"
datepicker-is-class="
'btn-info' for selectedDt,
'active' for activeDt
on dt"
ng-click="select(dt.date)"
ng-disabled="::dt.disabled"
tabindex="-1"><span
@@ -10,7 +10,10 @@
<tr ng-repeat="row in rows track by $index">
<td ng-repeat="dt in row" class="text-center" role="gridcell" id="{{::dt.uid}}">
<button type="button" style="width:100%;" class="btn btn-default"
ng-class="{'btn-info': dt.selected, active: isActive(dt)}"
datepicker-is-class="
'btn-info' for selectedDt,
'active' for activeDt
on dt"
ng-click="select(dt.date)"
ng-disabled="::dt.disabled"
tabindex="-1"><span

0 comments on commit a14c6dd

Please sign in to comment.