Skip to content

Commit cac4ca8

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 angular#8234
1 parent b9e899c commit cac4ca8

File tree

2 files changed

+93
-1
lines changed

2 files changed

+93
-1
lines changed

src/ng/directive/input.js

+48-1
Original file line numberDiff line numberDiff line change
@@ -2056,6 +2056,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
20562056
var viewValue = ctrl.$$lastCommittedViewValue;
20572057
var modelValue = viewValue;
20582058
var parserValid = isUndefined(modelValue) ? undefined : true;
2059+
var flushPendingClassChanges = schedulePendingClassChanges($scope, ctrl, $element, $animate);
20592060

20602061
if (parserValid) {
20612062
for(var i = 0; i < ctrl.$parsers.length; i++) {
@@ -2092,6 +2093,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
20922093
ctrl.$$writeModelToScope();
20932094
}
20942095
}
2096+
2097+
flushPendingClassChanges();
20952098
};
20962099

20972100
this.$$writeModelToScope = function() {
@@ -2197,7 +2200,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
21972200
// TODO(perf): why not move this to the action fn?
21982201
if (modelValue !== ctrl.$modelValue) {
21992202
ctrl.$modelValue = modelValue;
2200-
2203+
var flushPendingClassChanges = schedulePendingClassChanges($scope, ctrl, $element, $animate);
22012204
var formatters = ctrl.$formatters,
22022205
idx = formatters.length;
22032206

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

22122215
ctrl.$$runValidators(undefined, modelValue, viewValue, noop);
22132216
}
2217+
flushPendingClassChanges();
22142218
}
22152219

22162220
return modelValue;
22172221
});
22182222
}];
22192223

22202224

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

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

test/ng/directive/inputSpec.js

+45
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,51 @@ describe('NgModelController', function() {
892892
dealoc(element);
893893
}));
894894

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

0 commit comments

Comments
 (0)