Skip to content

Commit

Permalink
refactor(datepicker): optimize datepicker
Browse files Browse the repository at this point in the history
- Add new class manipulation helper directive that uses 1 watcher as opposed to watchers for each element with ng-class
- Optimize disabled state
- Optimize class binding

Closes angular-ui#2613
Closes angular-ui#3451
Closes angular-ui#3770
Closes angular-ui#5065

BREAKING CHANGE: For those using custom templates, the changes result in necessary changes being made to the templates to match the new class changing syntax
  • Loading branch information
chrisirhc authored and wesleycho committed Dec 30, 2015
1 parent f33ab7c commit 72f13ef
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 23 deletions.
61 changes: 44 additions & 17 deletions src/datepicker/datepicker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])
angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass', 'ui.bootstrap.position'])

.value('$datepickerSuppressError', false)

Expand Down Expand Up @@ -127,7 +127,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;
date = dateParser.fromTimezone(date, ngModelOptions.timezone);
Expand All @@ -139,14 +143,24 @@ 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);
return {
var dt = {
date: date,
label: dateFilter(date, format),
selected: model && this.compare(date, model) === 0,
disabled: this.isDisabled(date),
current: this.compare(date, new Date()) === 0,
customClass: this.customClass(date)
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) {
Expand Down Expand Up @@ -651,29 +665,42 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
}
});

angular.forEach(['minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function(key) {
angular.forEach(['datepickerMode', 'shortcutPropagation'], function(key) {
if (attrs[key]) {
var getAttribute = $parse(attrs[key]);
scope.$parent.$watch(getAttribute, function(value) {
if (key === 'minDate' || key === 'maxDate') {
cache[key] = angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium'));
}
scope.watchData[key] = cache[key] || value;
if (key === 'initDate') {
scope.watchData[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone);
var propConfig = {
get: function() {
return getAttribute(scope.$parent);
}
});
};

datepickerEl.attr(cameltoDash(key), 'watchData.' + key);

// Propagate changes from datepicker to outside
if (key === 'datepickerMode') {
var setAttribute = getAttribute.assign;
scope.$watch('watchData.' + key, function(value, oldvalue) {
if (angular.isFunction(setAttribute) && value !== oldvalue) {
setAttribute(scope.$parent, value);
}
});
propConfig.set = function(v) {
setAttribute(scope.$parent, v);
};
}

Object.defineProperty(scope.watchData, key, propConfig);
}
});

angular.forEach(['minDate', 'maxDate', 'initDate'], function(key) {
if (attrs[key]) {
var getAttribute = $parse(attrs[key]);

scope.$parent.$watch(getAttribute, function(value) {
if (key === 'minDate' || key === 'maxDate') {
cache[key] = angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium'));
}

scope.watchData[key] = cache[key] || dateParser.fromTimezone(new Date(value), ngModelOptions.timezone);
});

datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
}
});

Expand Down
98 changes: 98 additions & 0 deletions src/isClass/isClass.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Avoiding use of ng-class as it creates a lot of watchers when a class is to be applied to
// at most one element.
angular.module('ui.bootstrap.isClass', [])
.directive('uibIsClass', [
'$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.uibIsClass.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 = {};
}
}
}
};
}]);
80 changes: 80 additions & 0 deletions src/isClass/test/isClass.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
describe('uibIsClass', function() {
var $rootScope;

beforeEach(module('ui.bootstrap.isClass'));
beforeEach(inject(function($compile, _$rootScope_) {
$rootScope = _$rootScope_;
$rootScope.activeClass = 'active';
$rootScope.items = [1, 2, 3];
element = $compile('<div><div ng-repeat="item in items" ' +
'uib-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('');
});
});
13 changes: 11 additions & 2 deletions template/datepicker/day.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@
<tbody>
<tr class="uib-weeks" ng-repeat="row in rows track by $index">
<td ng-if="showWeeks" class="text-center h6"><em>{{ weekNumbers[$index] }}</em></td>
<td ng-repeat="dt in row track by dt.date" class="uib-day text-center" role="gridcell" id="{{::dt.uid}}" ng-class="::dt.customClass">
<button type="button" style="min-width:100%;" class="btn btn-default btn-sm" ng-class="{'btn-info': dt.selected, active: isActive(dt)}" ng-click="select(dt.date)" ng-disabled="dt.disabled" tabindex="-1"><span ng-class="::{'text-muted': dt.secondary, 'text-info': dt.current}">{{::dt.label}}</span></button>
<td ng-repeat="dt in row" class="uib-day text-center" role="gridcell"
id="{{::dt.uid}}"
ng-class="::dt.customClass">
<button type="button" style="min-width:100%;" class="btn btn-default btn-sm"
uib-is-class="
'btn-info' for selectedDt,
'active' for activeDt
on dt"
ng-click="select(dt.date)"
ng-disabled="::dt.disabled"
tabindex="-1"><span ng-class="::{'text-muted': dt.secondary, 'text-info': dt.current}">{{::dt.label}}</span></button>
</td>
</tr>
</tbody>
Expand Down
13 changes: 11 additions & 2 deletions template/datepicker/month.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@
</thead>
<tbody>
<tr class="uib-months" ng-repeat="row in rows track by $index">
<td ng-repeat="dt in row track by dt.date" class="uib-month text-center" role="gridcell" id="{{::dt.uid}}" ng-class="::dt.customClass">
<button type="button" style="min-width:100%;" class="btn btn-default" ng-class="{'btn-info': dt.selected, active: isActive(dt)}" ng-click="select(dt.date)" ng-disabled="dt.disabled" tabindex="-1"><span ng-class="::{'text-info': dt.current}">{{::dt.label}}</span></button>
<td ng-repeat="dt in row" class="uib-month text-center" role="gridcell"
id="{{::dt.uid}}"
ng-class="::dt.customClass">
<button type="button" style="min-width:100%;" class="btn btn-default"
uib-is-class="
'btn-info' for selectedDt,
'active' for activeDt
on dt"
ng-click="select(dt.date)"
ng-disabled="::dt.disabled"
tabindex="-1"><span ng-class="::{'text-info': dt.current}">{{::dt.label}}</span></button>
</td>
</tr>
</tbody>
Expand Down
13 changes: 11 additions & 2 deletions template/datepicker/year.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@
</thead>
<tbody>
<tr class="uib-years" ng-repeat="row in rows track by $index">
<td ng-repeat="dt in row track by dt.date" class="uib-year text-center" role="gridcell" id="{{::dt.uid}}" ng-class="::dt.customClass">
<button type="button" style="min-width:100%;" class="btn btn-default" ng-class="{'btn-info': dt.selected, active: isActive(dt)}" ng-click="select(dt.date)" ng-disabled="dt.disabled" tabindex="-1"><span ng-class="::{'text-info': dt.current}">{{::dt.label}}</span></button>
<td ng-repeat="dt in row" class="uib-year text-center" role="gridcell"
id="{{::dt.uid}}"
ng-class="::dt.customClass">
<button type="button" style="min-width:100%;" class="btn btn-default"
uib-is-class="
'btn-info' for selectedDt,
'active' for activeDt
on dt"
ng-click="select(dt.date)"
ng-disabled="::dt.disabled"
tabindex="-1"><span ng-class="::{'text-info': dt.current}">{{::dt.label}}</span></button>
</td>
</tr>
</tbody>
Expand Down

0 comments on commit 72f13ef

Please sign in to comment.