diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index afa8347e23c6..9644ab21a0d7 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1039,7 +1039,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { element.on('change', listener); ctrl.$render = function() { - element.val(ctrl.$isEmpty(ctrl.$modelValue) ? '' : ctrl.$viewValue); + element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; } @@ -1149,10 +1149,10 @@ function createDateInputType(type, regexp, parseDate, format) { }); ctrl.$formatters.push(function(value) { - if (!ctrl.$isEmpty(value)) { - if (!isDate(value)) { - throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); - } + if (value && !isDate(value)) { + throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); + } + if (isValidDate(value)) { previousDate = value; if (previousDate && timezone === 'UTC') { var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); @@ -1161,14 +1161,14 @@ function createDateInputType(type, regexp, parseDate, format) { return $filter('date')(value, format, timezone); } else { previousDate = null; + return ''; } - return ''; }); if (isDefined(attr.min) || attr.ngMin) { var minVal; ctrl.$validators.min = function(value) { - return ctrl.$isEmpty(value) || isUndefined(minVal) || parseDate(value) >= minVal; + return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal; }; attr.$observe('min', function(val) { minVal = parseObservedDateValue(val); @@ -1179,18 +1179,18 @@ function createDateInputType(type, regexp, parseDate, format) { if (isDefined(attr.max) || attr.ngMax) { var maxVal; ctrl.$validators.max = function(value) { - return ctrl.$isEmpty(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; + return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; }; attr.$observe('max', function(val) { maxVal = parseObservedDateValue(val); ctrl.$validate(); }); } - // Override the standard $isEmpty to detect invalid dates as well - ctrl.$isEmpty = function(value) { + + function isValidDate(value) { // Invalid Date: getTime() returns NaN - return !value || (value.getTime && value.getTime() !== value.getTime()); - }; + return value && !(value.getTime && value.getTime() !== value.getTime()); + } function parseObservedDateValue(val) { return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; @@ -1274,7 +1274,8 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { stringBasedInputType(ctrl); ctrl.$$parserName = 'url'; - ctrl.$validators.url = function(value) { + ctrl.$validators.url = function(modelValue, viewValue) { + var value = modelValue || viewValue; return ctrl.$isEmpty(value) || URL_REGEXP.test(value); }; } @@ -1286,7 +1287,8 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { stringBasedInputType(ctrl); ctrl.$$parserName = 'email'; - ctrl.$validators.email = function(value) { + ctrl.$validators.email = function(modelValue, viewValue) { + var value = modelValue || viewValue; return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); }; } @@ -1340,9 +1342,11 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt element[0].checked = ctrl.$viewValue; }; - // Override the standard `$isEmpty` because an empty checkbox is never equal to the trueValue + // Override the standard `$isEmpty` because the $viewValue of an empty checkbox is always set to `false` + // This is because of the parser below, which compares the `$modelValue` with `trueValue` to convert + // it to a boolean. ctrl.$isEmpty = function(value) { - return value !== trueValue; + return value === false; }; ctrl.$formatters.push(function(value) { @@ -1810,17 +1814,18 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @name ngModel.NgModelController#$isEmpty * * @description - * This is called when we need to determine if the value of the input is empty. + * This is called when we need to determine if the value of an input is empty. * * For instance, the required directive does this to work out if the input has data or not. + * * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. * * You can override this for input directives whose concept of being empty is different to the * default. The `checkboxInputType` directive does this because in its case a value of `false` * implies empty. * - * @param {*} value Model value to check. - * @returns {boolean} True if `value` is empty. + * @param {*} value The value of the input to check for emptiness. + * @returns {boolean} True if `value` is "empty". */ this.$isEmpty = function(value) { return isUndefined(value) || value === '' || value === null || value !== value; @@ -2647,8 +2652,8 @@ var requiredDirective = function() { if (!ctrl) return; attr.required = true; // force truthy in case we are on non input element - ctrl.$validators.required = function(value) { - return !attr.required || !ctrl.$isEmpty(value); + ctrl.$validators.required = function(modelValue, viewValue) { + return !attr.required || !ctrl.$isEmpty(viewValue); }; attr.$observe('required', function() { @@ -2723,7 +2728,7 @@ var minlengthDirective = function() { ctrl.$validate(); }); ctrl.$validators.minlength = function(modelValue, viewValue) { - return ctrl.$isEmpty(modelValue) || viewValue.length >= minlength; + return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength; }; } }; diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index b2ab27ac9469..e579eb0f8db3 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -2290,6 +2290,22 @@ describe('input', function() { }); + it('should render the $viewValue when $modelValue is empty', function() { + compileInput(''); + + var ctrl = inputElm.controller('ngModel'); + + ctrl.$modelValue = null; + + expect(ctrl.$isEmpty(ctrl.$modelValue)).toBe(true); + + ctrl.$viewValue = 'abc'; + ctrl.$render(); + + expect(inputElm.val()).toBe('abc'); + }); + + describe('pattern', function() { it('should validate in-lined pattern', function() {