Skip to content

Commit

Permalink
fix(ngModel): minimize jank when toggling CSS classes during validation
Browse files Browse the repository at this point in the history
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
  • Loading branch information
caitp committed Sep 25, 2014
1 parent b9e899c commit bbb5151
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 1 deletion.
50 changes: 49 additions & 1 deletion src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -2053,9 +2053,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
};

this.$$parseAndValidate = function() {
var pendingClassChanges = null;
var viewValue = ctrl.$$lastCommittedViewValue;
var modelValue = viewValue;
var parserValid = isUndefined(modelValue) ? undefined : true;
var flushPendingClassChanges = schedulePendingClassChanges($scope, ctrl, $element, $animate);

if (parserValid) {
for(var i = 0; i < ctrl.$parsers.length; i++) {
Expand Down Expand Up @@ -2092,6 +2094,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
ctrl.$$writeModelToScope();
}
}

flushPendingClassChanges();
};

this.$$writeModelToScope = function() {
Expand Down Expand Up @@ -2197,7 +2201,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
// TODO(perf): why not move this to the action fn?
if (modelValue !== ctrl.$modelValue) {
ctrl.$modelValue = modelValue;

var flushPendingClassChanges = schedulePendingClassChanges($scope, ctrl, $element, $animate);
var formatters = ctrl.$formatters,
idx = formatters.length;

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

ctrl.$$runValidators(undefined, modelValue, viewValue, noop);
}
flushPendingClassChanges();
}

return modelValue;
});
}];


function schedulePendingClassChanges(scope, ctrl, element, animate) {
if (!ctrl.$$pendingClassChanges) {
ctrl.$$pendingClassChanges = {};

if (scope.$$phase || scope.$root.$$phase) {
scope.$$postDigest(flushPendingClassChangesImmediately);
} else {
return flushPendingClassChangesImmediately;
}
}
return noop;

function flushPendingClassChangesImmediately() {
flushPendingClassChanges(animate, element, ctrl.$$pendingClassChanges);
ctrl.$$pendingClassChanges = null;
}
}


function flushPendingClassChanges($animate, element, pendingChanges) {
var keys = Object.keys(pendingChanges);

for (var i=0, ii = keys.length; i < ii; ++i) {
var key = keys[i];
var value = pendingChanges[key];
if (value < 0) $animate.removeClass(element, key);
else if (value > 0) $animate.addClass(element, key);
}
}


/**
* @ngdoc directive
* @name ngModel
Expand Down Expand Up @@ -3037,6 +3073,18 @@ function addSetValidityMethod(context) {
}

function cachedToggleClass(className, switchValue) {
var pendingChanges = ctrl.$$pendingClassChanges;
if (pendingChanges) {
if (switchValue) {
pendingChanges[className] = 1;
classCache[className] = true;
} else {
pendingChanges[className] = -1;
classCache[className] = false;
}
return;
}

if (switchValue && !classCache[className]) {
$animate.addClass($element, className);
classCache[className] = true;
Expand Down
45 changes: 45 additions & 0 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,51 @@ describe('NgModelController', function() {
dealoc(element);
}));


it('should minimize janky setting of classes during $validate() and ngModelWatch', inject(function($animate, $compile, $rootScope) {
var addClass = $animate.addClass;
var removeClass = $animate.removeClass;
var addClassCallCount = 0;
var removeClassCallCount = 0;
var input;
$animate.addClass = function(element, className) {
// Don't worry about classes that the input already has.
if (input && element[0] === input[0] && (' ' + element.attr('class') + ' ').indexOf(' ' + className + ' ') < 0) {
++addClassCallCount;
}
return addClass.call($animate, element, className);
};

$animate.removeClass = function(element, className) {
// Don't worry about classes that the input doesn't have.
if (input && element[0] === input[0] && (' ' + element.attr('class') + ' ').indexOf(' ' + className + ' ') !== -1) {
++removeClassCallCount;
}
return removeClass.call($animate, element, className);
};

dealoc(element);

$rootScope.value = "123456789";
element = $compile(
'<form name="form">' +
'<input type="text" ng-model="value" name="alias" ng-maxlength="10">' +
'</form>'
)($rootScope);

var form = $rootScope.form;
input = element.children().eq(0);

$rootScope.$digest();

expect(input).toBeValid();
expect(input).not.toHaveClass('ng-invalid-maxlength');
expect(input).toHaveClass('ng-valid-maxlength');
expect(addClassCallCount).toBe(2);
expect(removeClassCallCount).toBe(0);

dealoc(element);
}));
});
});

Expand Down

0 comments on commit bbb5151

Please sign in to comment.