Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 078ba1b

Browse files
committed
fix(ngModel): minimize jank when toggling CSS classes during validation
Previously, class toggling would always occur immediately. This causes problems in cases where class changes happen super frequently, and can result in flickering in some browsers which do not handle this jank well. Closes #8234
1 parent b9e899c commit 078ba1b

File tree

2 files changed

+102
-1
lines changed

2 files changed

+102
-1
lines changed

src/ng/directive/input.js

+49-1
Original file line numberDiff line numberDiff line change
@@ -2053,9 +2053,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
20532053
};
20542054

20552055
this.$$parseAndValidate = function() {
2056+
var pendingClassChanges = null;
20562057
var viewValue = ctrl.$$lastCommittedViewValue;
20572058
var modelValue = viewValue;
20582059
var parserValid = isUndefined(modelValue) ? undefined : true;
2060+
var flushPendingClassChanges = schedulePendingClassChanges($scope, ctrl, $element, $animate);
20592061

20602062
if (parserValid) {
20612063
for(var i = 0; i < ctrl.$parsers.length; i++) {
@@ -2092,6 +2094,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
20922094
ctrl.$$writeModelToScope();
20932095
}
20942096
}
2097+
2098+
flushPendingClassChanges();
20952099
};
20962100

20972101
this.$$writeModelToScope = function() {
@@ -2197,7 +2201,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
21972201
// TODO(perf): why not move this to the action fn?
21982202
if (modelValue !== ctrl.$modelValue) {
21992203
ctrl.$modelValue = modelValue;
2200-
2204+
var flushPendingClassChanges = schedulePendingClassChanges($scope, ctrl, $element, $animate);
22012205
var formatters = ctrl.$formatters,
22022206
idx = formatters.length;
22032207

@@ -2211,13 +2215,45 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
22112215

22122216
ctrl.$$runValidators(undefined, modelValue, viewValue, noop);
22132217
}
2218+
flushPendingClassChanges();
22142219
}
22152220

22162221
return modelValue;
22172222
});
22182223
}];
22192224

22202225

2226+
function schedulePendingClassChanges(scope, ctrl, element, animate) {
2227+
if (!ctrl.$$pendingClassChanges) {
2228+
ctrl.$$pendingClassChanges = {};
2229+
2230+
if (scope.$$phase || scope.$root.$$phase) {
2231+
scope.$$postDigest(flushPendingClassChangesImmediately);
2232+
} else {
2233+
return flushPendingClassChangesImmediately;
2234+
}
2235+
}
2236+
return noop;
2237+
2238+
function flushPendingClassChangesImmediately() {
2239+
flushPendingClassChanges(animate, element, ctrl.$$pendingClassChanges);
2240+
ctrl.$$pendingClassChanges = null;
2241+
}
2242+
}
2243+
2244+
2245+
function flushPendingClassChanges($animate, element, pendingChanges) {
2246+
var keys = Object.keys(pendingChanges);
2247+
2248+
for (var i=0, ii = keys.length; i < ii; ++i) {
2249+
var key = keys[i];
2250+
var value = pendingChanges[key];
2251+
if (value < 0) $animate.removeClass(element, key);
2252+
else if (value > 0) $animate.addClass(element, key);
2253+
}
2254+
}
2255+
2256+
22212257
/**
22222258
* @ngdoc directive
22232259
* @name ngModel
@@ -3037,6 +3073,18 @@ function addSetValidityMethod(context) {
30373073
}
30383074

30393075
function cachedToggleClass(className, switchValue) {
3076+
var pendingChanges = ctrl.$$pendingClassChanges;
3077+
if (pendingChanges) {
3078+
if (switchValue) {
3079+
pendingChanges[className] = 1;
3080+
classCache[className] = true;
3081+
} else {
3082+
pendingChanges[className] = -1;
3083+
classCache[className] = false;
3084+
}
3085+
return;
3086+
}
3087+
30403088
if (switchValue && !classCache[className]) {
30413089
$animate.addClass($element, className);
30423090
classCache[className] = true;

test/ng/directive/inputSpec.js

+53
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
describe('NgModelController', function() {
44
/* global NgModelController: false */
55
var ctrl, scope, ngModelAccessor, element, parentFormCtrl;
6+
var directive;
7+
8+
beforeEach(module(function($compileProvider) {
9+
directive = function() {
10+
return $compileProvider.directive.apply($compileProvider,
11+
Array.prototype.slice.call(arguments, 0));
12+
}
13+
}));
614

715
beforeEach(inject(function($rootScope, $controller) {
816
var attrs = {name: 'testAlias', ngModel: 'value'};
@@ -892,6 +900,51 @@ describe('NgModelController', function() {
892900
dealoc(element);
893901
}));
894902

903+
904+
it('should minimize janky setting of classes during $validate() and ngModelWatch', inject(function($animate, $compile, $rootScope) {
905+
var addClass = $animate.addClass;
906+
var removeClass = $animate.removeClass;
907+
var addClassCallCount = 0;
908+
var removeClassCallCount = 0;
909+
var input;
910+
$animate.addClass = function(element, className) {
911+
// Don't worry about classes that the input already has.
912+
if (input && element[0] === input[0] && (' ' + element.attr('class') + ' ').indexOf(' ' + className + ' ') < 0) {
913+
++addClassCallCount;
914+
}
915+
return addClass.call($animate, element, className);
916+
}
917+
918+
$animate.removeClass = function(element, className) {
919+
// Don't worry about classes that the input doesn't have.
920+
if (input && element[0] === input[0] && (' ' + element.attr('class') + ' ').indexOf(' ' + className + ' ') !== -1) {
921+
++removeClassCallCount;
922+
}
923+
return removeClass.call($animate, element, className);
924+
}
925+
926+
dealoc(element);
927+
928+
$rootScope.value = "123456789";
929+
element = $compile(
930+
'<form name="form">' +
931+
'<input type="text" ng-model="value" name="alias" ng-maxlength="10">' +
932+
'</form>'
933+
)($rootScope);
934+
935+
var form = $rootScope.form;
936+
input = element.children().eq(0);
937+
938+
$rootScope.$digest();
939+
940+
expect(input).toBeValid();
941+
expect(input).not.toHaveClass('ng-invalid-maxlength');
942+
expect(input).toHaveClass('ng-valid-maxlength');
943+
expect(addClassCallCount).toBe(2);
944+
expect(removeClassCallCount).toBe(0);
945+
946+
dealoc(element);
947+
}));
895948
});
896949
});
897950

0 commit comments

Comments
 (0)