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

feat(ngModel): provide ng-empty and ng-not-empty CSS classes #12848

Merged
merged 2 commits into from
Sep 22, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/ng/directive/ngModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ var VALID_CLASS = 'ng-valid',
DIRTY_CLASS = 'ng-dirty',
UNTOUCHED_CLASS = 'ng-untouched',
TOUCHED_CLASS = 'ng-touched',
PENDING_CLASS = 'ng-pending';
PENDING_CLASS = 'ng-pending',
EMPTY_CLASS = 'ng-empty',
NOT_EMPTY_CLASS = 'ng-not-empty';

var ngModelMinErr = minErr('ngModel');

Expand Down Expand Up @@ -316,6 +318,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
return isUndefined(value) || value === '' || value === null || value !== value;
};

this.$$updateEmptyClasses = function(value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be simplified by always using ctrl.$viewValue instead of passing it as an arg. It should only be the viewValue anyway. You'll have to change the order on line https://github.com/angular/angular.js/pull/12848/files#diff-ae1963e24d6328c3aa019c0c1b9f4432R851 though, because otherwise it'll use the old viewValue

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it would have to use $$lastCommittedViewValue and we would have to change the order as you suggest. I am not that keen on doing this though as I don't feel that not passing in an argument makes it that much simpler.

if (ctrl.$isEmpty(value)) {
$animate.removeClass($element, NOT_EMPTY_CLASS);
$animate.addClass($element, EMPTY_CLASS);
} else {
$animate.removeClass($element, EMPTY_CLASS);
$animate.addClass($element, NOT_EMPTY_CLASS);
}
};


var currentValidationRunId = 0;

/**
Expand Down Expand Up @@ -652,6 +665,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
return;
}
ctrl.$$updateEmptyClasses(viewValue);
ctrl.$$lastCommittedViewValue = viewValue;

// change to dirty
Expand Down Expand Up @@ -834,6 +848,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
viewValue = formatters[idx](viewValue);
}
if (ctrl.$viewValue !== viewValue) {
ctrl.$$updateEmptyClasses(viewValue);
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
ctrl.$render();

Expand Down Expand Up @@ -864,7 +879,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* require.
* - Providing validation behavior (i.e. required, number, email, url).
* - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors).
* - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations.
* - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`,
* `ng-untouched`, `ng-empty`, `ng-not-empty`) including animations.
* - Registering the control with its parent {@link ng.directive:form form}.
*
* Note: `ngModel` will try to bind to the property given by evaluating the expression on the
Expand Down Expand Up @@ -905,13 +921,16 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* - `ng-touched`: the control has been blurred
* - `ng-untouched`: the control hasn't been blurred
* - `ng-pending`: any `$asyncValidators` are unfulfilled
* - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined
* by the {@link ngModel.NgModelController#$isEmpty} method
* - `ng-not-empty`: the view contains a non-empty value
*
* Keep in mind that ngAnimate can detect each of these classes when added and removed.
*
* ## Animation Hooks
*
* Animations within models are triggered when any of the associated CSS classes are added and removed
* on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`,
* on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`,
* `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself.
* The animations that are triggered within ngModel are similar to how they work in ngClass and
* animations can be hooked into using CSS transitions, keyframes as well as JS animations.
Expand Down
2 changes: 2 additions & 0 deletions test/helpers/matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ beforeEach(function() {
}

this.addMatchers({
toBeEmpty: cssMatcher('ng-empty', 'ng-not-empty'),
toBeNotEmpty: cssMatcher('ng-not-empty', 'ng-empty'),
toBeInvalid: cssMatcher('ng-invalid', 'ng-valid'),
toBeValid: cssMatcher('ng-valid', 'ng-invalid'),
toBeDirty: cssMatcher('ng-dirty', 'ng-pristine'),
Expand Down
68 changes: 26 additions & 42 deletions test/ng/directive/ngModelSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1182,46 +1182,6 @@ describe('ngModel', function() {
}));


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) {
if (input && element[0] === input[0]) ++addClassCallCount;
return addClass.call($animate, element, className);
};

$animate.removeClass = function(element, className) {
if (input && element[0] === input[0]) ++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(1);
expect(removeClassCallCount).toBe(0);

dealoc(element);
}));


it('should always use the most recent $viewValue for validation', function() {
ctrl.$parsers.push(function(value) {
if (value && value.substr(-1) === 'b') {
Expand Down Expand Up @@ -1368,6 +1328,28 @@ describe('ngModel', function() {
describe('CSS classes', function() {
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;

it('should set ng-empty or ng-not-empty when the view value changes',
inject(function($compile, $rootScope, $sniffer) {

var element = $compile('<input ng-model="value" />')($rootScope);

$rootScope.$digest();
expect(element).toBeEmpty();

$rootScope.value = 'XXX';
$rootScope.$digest();
expect(element).toBeNotEmpty();

element.val('');
browserTrigger(element, $sniffer.hasEvent('input') ? 'input' : 'change');
expect(element).toBeEmpty();

element.val('YYY');
browserTrigger(element, $sniffer.hasEvent('input') ? 'input' : 'change');
expect(element).toBeNotEmpty();
}));


it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty, ng-untouched, ng-touched)',
inject(function($compile, $rootScope, $sniffer) {
var element = $compile('<input type="email" ng-model="value" />')($rootScope);
Expand Down Expand Up @@ -1733,8 +1715,10 @@ describe('ngModel', function() {
model.$setViewValue('some dirty value');

var animations = findElementAnimations(input, $animate.queue);
assertValidAnimation(animations[0], 'removeClass', 'ng-pristine');
assertValidAnimation(animations[1], 'addClass', 'ng-dirty');
assertValidAnimation(animations[0], 'removeClass', 'ng-empty');
assertValidAnimation(animations[1], 'addClass', 'ng-not-empty');
assertValidAnimation(animations[2], 'removeClass', 'ng-pristine');
assertValidAnimation(animations[3], 'addClass', 'ng-dirty');
}));


Expand Down