From 0049a9bdbb2b8206219248c081b8ba75830288ad Mon Sep 17 00:00:00 2001 From: christrude Date: Fri, 2 Dec 2016 13:30:11 -0500 Subject: [PATCH 1/2] Remove mdMaxlength directive forcing ng-trim Adding ng-trim="false" to an input will cause the 'required' state to be fulfilled by empty spaces, essentially allowing a user to put in no content and pass required validation. Allowing angular to trim, and trimming manually in the renderCharCount function works better (IMO). --- src/components/input/input.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/input/input.js b/src/components/input/input.js index ceda40ce61..64685e9c81 100644 --- a/src/components/input/input.js +++ b/src/components/input/input.js @@ -664,8 +664,9 @@ function mdMaxlengthDirective($animate, $mdUtil) { // Stop model from trimming. This makes it so whitespace // over the maxlength still counts as invalid. - attr.$set('ngTrim', 'false'); - + // Edit: Setting the trim is not needed, as the whitespace over maxlength will be trimmed + // attr.$set('ngTrim', 'false'); + scope.$watch(attr.mdMaxlength, function(value) { maxlength = value; if (angular.isNumber(value) && value > 0) { @@ -687,7 +688,7 @@ function mdMaxlengthDirective($animate, $mdUtil) { // Force the value into a string since it may be a number, // which does not have a length property. - charCountEl.text(String(element.val() || value || '').length + ' / ' + maxlength); + charCountEl.text(String(element.val().trim || value || '').length + ' / ' + maxlength); return value; } } From 147551d5f695f0bfd2c5db3aaa873916c04ae0eb Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Mon, 26 Feb 2018 23:21:55 -0500 Subject: [PATCH 2/2] feat(mdMaxlength): support use with required and ng-trim="true" no longer force ng-trim="false" trim the value by default or based on the ng-trim attribute don't trim password inputs to align with AngularJS behavior Fixes #10082 --- .../autocomplete/js/autocompleteDirective.js | 2 +- src/components/input/input.js | 57 ++++++++++---- src/components/input/input.spec.js | 74 +++++++++++++++++++ 3 files changed, 118 insertions(+), 15 deletions(-) diff --git a/src/components/autocomplete/js/autocompleteDirective.js b/src/components/autocomplete/js/autocompleteDirective.js index eacfd4b516..45c1bfede8 100644 --- a/src/components/autocomplete/js/autocompleteDirective.js +++ b/src/components/autocomplete/js/autocompleteDirective.js @@ -128,7 +128,7 @@ angular * When the dropdown doesn't fit into the viewport, the dropdown will shrink * as much as possible. * @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`, `bottom`. - * @param {string=} ng-trim If set to false, the search text will be not trimmed automatically. + * @param {boolean=} ng-trim If set to false, the search text will be not trimmed automatically. * Defaults to true. * @param {string=} ng-pattern Adds the pattern validator to the ngModel of the search text. * See the [ngPattern Directive](https://docs.angularjs.org/api/ng/directive/ngPattern) diff --git a/src/components/input/input.js b/src/components/input/input.js index 64685e9c81..14b52e4b3e 100644 --- a/src/components/input/input.js +++ b/src/components/input/input.js @@ -190,8 +190,8 @@ function labelDirective() { * The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't * want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength` * or maxlength attributes.

- * **Note:** Only valid for text/string inputs (not numeric). - * + * @param {boolean=} ng-trim If set to false, the input text will be not trimmed automatically. + * Defaults to true. * @param {string=} aria-label Aria-label is required when no label is present. A warning message * will be logged in the console if not present. * @param {string=} placeholder An alternative approach to using aria-label when the label is not @@ -639,7 +639,8 @@ function mdMaxlengthDirective($animate, $mdUtil) { var ngModelCtrl = ctrls[0]; var containerCtrl = ctrls[1]; var charCountEl, errorsSpacer; - + var ngTrim = angular.isDefined(attr.ngTrim) ? $mdUtil.parseAttributeBoolean(attr.ngTrim) : true; + var isPasswordInput = attr.type === 'password'; ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) { if (!angular.isNumber(maxlength) || maxlength < 0) { @@ -650,7 +651,24 @@ function mdMaxlengthDirective($animate, $mdUtil) { // Using the $validators for triggering the update works very well. renderCharCount(); - return ( modelValue || element.val() || viewValue || '' ).length <= maxlength; + var elementVal = element.val() || viewValue; + if (elementVal === undefined || elementVal === null) { + elementVal = ''; + } + elementVal = ngTrim && !isPasswordInput && angular.isString(elementVal) ? elementVal.trim() : elementVal; + // Force the value into a string since it may be a number, + // which does not have a length property. + return String(elementVal).length <= maxlength; + }; + + /** + * Override the default NgModelController $isEmpty check to take ng-trim, password inputs, + * etc. into account. + * @param value {*} the input's value + * @returns {boolean} true if the input's value should be considered empty, false otherwise + */ + ngModelCtrl.$isEmpty = function(value) { + return calculateInputValueLength(value) === 0; }; // Wait until the next tick to ensure that the input has setup the errors spacer where we will @@ -662,10 +680,9 @@ function mdMaxlengthDirective($animate, $mdUtil) { // Append our character counter inside the errors spacer errorsSpacer.append(charCountEl); - // Stop model from trimming. This makes it so whitespace - // over the maxlength still counts as invalid. - // Edit: Setting the trim is not needed, as the whitespace over maxlength will be trimmed - // attr.$set('ngTrim', 'false'); + attr.$observe('ngTrim', function (value) { + ngTrim = angular.isDefined(value) ? $mdUtil.parseAttributeBoolean(value) : true; + }); scope.$watch(attr.mdMaxlength, function(value) { maxlength = value; @@ -680,16 +697,28 @@ function mdMaxlengthDirective($animate, $mdUtil) { }); }); - function renderCharCount(value) { - // If we have not been initialized or appended to the body yet; do not render - if (!charCountEl || !charCountEl.parent) { - return value; + /** + * Calculate the input value's length after coercing it to a string + * and trimming it if appropriate. + * @param value {*} the input's value + * @returns {number} calculated length of the input's value + */ + function calculateInputValueLength(value) { + value = ngTrim && !isPasswordInput && angular.isString(value) ? value.trim() : value; + if (value === undefined || value === null) { + value = ''; } + return String(value).length; + } + function renderCharCount() { + // If we have not been initialized or appended to the body yet; do not render. + if (!charCountEl || !charCountEl.parent()) { + return; + } // Force the value into a string since it may be a number, // which does not have a length property. - charCountEl.text(String(element.val().trim || value || '').length + ' / ' + maxlength); - return value; + charCountEl.text(calculateInputValueLength(element.val()) + ' / ' + maxlength); } } } diff --git a/src/components/input/input.spec.js b/src/components/input/input.spec.js index feb5e49e7a..86962342f7 100644 --- a/src/components/input/input.spec.js +++ b/src/components/input/input.spec.js @@ -393,6 +393,80 @@ describe('md-input-container directive', function() { expect(pageScope.form.foo.$error['md-maxlength']).toBeFalsy(); expect(getCharCounter(el).length).toBe(0); }); + + it('should not accept spaces for required inputs by default', function() { + var el = $compile( + '
' + + ' ' + + ' ' + + ' ' + + '
')(pageScope); + var input = el.find('input'); + + pageScope.$apply('foo = ""'); + pageScope.$apply('max = 1'); + + // Flush any pending $mdUtil.nextTick calls + $timeout.flush(); + + expect(input.hasClass('ng-invalid')).toBe(true); + expect(input.hasClass('ng-invalid-required')).toBe(true); + expect(pageScope.form.foo.$error['md-maxlength']).toBeFalsy(); + + pageScope.$apply('foo = " "'); + expect(input.hasClass('ng-invalid')).toBe(true); + expect(input.hasClass('ng-invalid-required')).toBe(true); + expect(pageScope.form.foo.$error['required']).toBeTruthy(); + expect(pageScope.form.foo.$error['md-maxlength']).toBeFalsy(); + }); + + it('should not trim spaces for required password inputs', function() { + var el = $compile( + '
' + + ' ' + + ' ' + + ' ' + + '
')(pageScope); + var input = el.find('input'); + + pageScope.$apply('foo = ""'); + pageScope.$apply('max = 1'); + + // Flush any pending $mdUtil.nextTick calls + $timeout.flush(); + + expect(input.hasClass('ng-invalid')).toBe(true); + expect(input.hasClass('ng-invalid-required')).toBe(true); + expect(pageScope.form.foo.$error['md-maxlength']).toBeFalsy(); + + pageScope.$apply('foo = " "'); + expect(input.hasClass('ng-invalid')).toBe(true); + expect(input.hasClass('ng-invalid-required')).toBe(false); + expect(pageScope.form.foo.$error['required']).toBeFalsy(); + expect(pageScope.form.foo.$error['md-maxlength']).toBeTruthy(); + }); + + it('should respect ng-trim="false"', function() { + var el = $compile( + '
' + + ' ' + + ' ' + + ' ' + + '
')(pageScope); + + pageScope.$apply('foo = ""'); + pageScope.$apply('max = 1'); + + // Flush any pending $mdUtil.nextTick calls + $timeout.flush(); + + expect(pageScope.form.foo.$error['required']).toBeTruthy(); + expect(pageScope.form.foo.$error['md-maxlength']).toBeFalsy(); + + pageScope.$apply('foo = " "'); + expect(pageScope.form.foo.$error['required']).toBeFalsy(); + expect(pageScope.form.foo.$error['md-maxlength']).toBeTruthy(); + }); }); it('should not add the md-input-has-placeholder class when the placeholder is transformed into a label', inject(function($rootScope, $compile) {