From 7a8fe6c6b1c7fa6762dee568eec48f1fdee67c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 9 Jun 2014 14:51:35 -0400 Subject: [PATCH] feat(attrs): trigger observers for specific ng-attributes When an observer is set to listen on the pattern, minlength or maxlength attributes via $attrs then the observer will also listen on the ngPattern, ngMinlength and the ngMaxlength attributes as well. Closes #7758 --- Gruntfile.js | 2 +- angularFiles.js | 2 +- src/.jshintrc | 3 + src/Angular.js | 3 + src/jqLite.js | 13 +++- src/ng/compile.js | 10 +++- .../directive/{booleanAttrs.js => attrs.js} | 23 +++++++ src/ng/directive/input.js | 44 +++++++------- test/ng/directive/inputSpec.js | 60 ++++++++++++++++++- test/ng/parseSpec.js | 1 - 10 files changed, 128 insertions(+), 33 deletions(-) rename src/ng/directive/{booleanAttrs.js => attrs.js} (94%) diff --git a/Gruntfile.js b/Gruntfile.js index cf64df364000..dc9a7c95341e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -234,7 +234,7 @@ module.exports = function(grunt) { 'src/**/*.js', 'test/**/*.js', '!test/ngScenario/DescribeSpec.js', - '!src/ng/directive/booleanAttrs.js', // legitimate xit here + '!src/ng/directive/attrs.js', // legitimate xit here '!src/ngScenario/**/*.js' ] }, diff --git a/angularFiles.js b/angularFiles.js index 80ed0f9e5462..60fdf3f8964a 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -44,7 +44,7 @@ angularFiles = { 'src/ng/directive/directives.js', 'src/ng/directive/a.js', - 'src/ng/directive/booleanAttrs.js', + 'src/ng/directive/attrs.js', 'src/ng/directive/form.js', 'src/ng/directive/input.js', 'src/ng/directive/ngBind.js', diff --git a/src/.jshintrc b/src/.jshintrc index 1202b64447a3..fc37b31ec226 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -34,6 +34,7 @@ "nodeName_": false, "uid": false, + "REGEX_STRING_REGEXP" : false, "lowercase": false, "uppercase": false, "manualLowercase": false, @@ -117,6 +118,7 @@ /* jqLite.js */ "BOOLEAN_ATTR": false, + "ALIASED_ATTR": false, "jqNextId": false, "camelCase": false, "jqLitePatchJQueryRemove": false, @@ -134,6 +136,7 @@ "jqLiteController": false, "jqLiteInheritedData": false, "getBooleanAttrName": false, + "getAliasedAttrName": false, "createEventHandler": false, "JQLitePrototype": false, "addEventListenerFn": false, diff --git a/src/Angular.js b/src/Angular.js index a81fe1c9aca7..725164808d9a 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -13,6 +13,7 @@ -angularModule, -nodeName_, -uid, + -REGEX_STRING_REGEXP, -lowercase, -uppercase, @@ -102,6 +103,8 @@ *
*/ +var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; + /** * @ngdoc function * @name angular.lowercase diff --git a/src/jqLite.js b/src/jqLite.js index 70093a1194a5..ec6ca09e9717 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -5,7 +5,8 @@ -JQLitePrototype, -addEventListenerFn, -removeEventListenerFn, - -BOOLEAN_ATTR + -BOOLEAN_ATTR, + -ALIASED_ATTR */ ////////////////////////////////// @@ -463,6 +464,11 @@ var BOOLEAN_ELEMENTS = {}; forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { BOOLEAN_ELEMENTS[uppercase(value)] = true; }); +var ALIASED_ATTR = { + 'ngMinlength' : 'minlength', + 'ngMaxlength' : 'maxlength', + 'ngPattern' : 'pattern' +}; function getBooleanAttrName(element, name) { // check dom last since we will most likely fail on name @@ -472,6 +478,11 @@ function getBooleanAttrName(element, name) { return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr; } +function getAliasedAttrName(element, name) { + var nodeName = element.nodeName; + return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name]; +} + forEach({ data: jqLiteData, inheritedData: jqLiteInheritedData, diff --git a/src/ng/compile.js b/src/ng/compile.js index 6814edb69a7f..300fba8d2a1a 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -729,13 +729,19 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { //is set through this function since it may cause $updateClass to //become unstable. - var booleanKey = getBooleanAttrName(this.$$element[0], key), + var node = this.$$element[0], + booleanKey = getBooleanAttrName(node, key), + aliasedKey = getAliasedAttrName(node, key), + observer = key, normalizedVal, nodeName; if (booleanKey) { this.$$element.prop(key, value); attrName = booleanKey; + } else if(aliasedKey) { + this[aliasedKey] = value; + observer = aliasedKey; } this[key] = value; @@ -768,7 +774,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // fire observers var $$observers = this.$$observers; - $$observers && forEach($$observers[key], function(fn) { + $$observers && forEach($$observers[observer], function(fn) { try { fn(value); } catch (e) { diff --git a/src/ng/directive/booleanAttrs.js b/src/ng/directive/attrs.js similarity index 94% rename from src/ng/directive/booleanAttrs.js rename to src/ng/directive/attrs.js index e98a61034063..9fe40d34b6ff 100644 --- a/src/ng/directive/booleanAttrs.js +++ b/src/ng/directive/attrs.js @@ -361,6 +361,29 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) { }; }); +// aliased input attrs are evaluated +forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { + ngAttributeAliasDirectives[ngAttr] = function() { + return { + priority: 100, + link: function(scope, element, attr) { + //special case ngPattern when a literal regular expression value + //is used as the expression (this way we don't have to watch anything). + if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") { + var match = attr.ngPattern.match(REGEX_STRING_REGEXP); + if (match) { + attr.$set("ngPattern", new RegExp(match[1], match[2])); + return; + } + } + + scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) { + attr.$set(ngAttr, value); + }); + } + }; + }; +}); // ng-src, ng-srcset, ng-href are interpolated forEach(['src', 'srcset', 'href'], function(attrName) { diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index a526fb553f89..554e930e5dbf 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -975,32 +975,28 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { }; // pattern validator - var pattern = attr.ngPattern, - patternValidator, - match; + if (attr.ngPattern) { + var regexp, patternExp = attr.ngPattern; + attr.$observe('pattern', function(regex) { + if(isString(regex)) { + var match = regex.match(REGEX_STRING_REGEXP); + if(match) { + regex = new RegExp(match[1], match[2]); + } + } - if (pattern) { - var validateRegex = function(regexp, value) { - return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value); - }; - match = pattern.match(/^\/(.*)\/([gim]*)$/); - if (match) { - pattern = new RegExp(match[1], match[2]); - patternValidator = function(value) { - return validateRegex(pattern, value); - }; - } else { - patternValidator = function(value) { - var patternObj = scope.$eval(pattern); + if (regex && !regex.test) { + throw minErr('ngPattern')('noregexp', + 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp, + regex, startingTag(element)); + } - if (!patternObj || !patternObj.test) { - throw minErr('ngPattern')('noregexp', - 'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern, - patternObj, startingTag(element)); - } - return validateRegex(patternObj, value); - }; - } + regexp = regex || undefined; + }); + + var patternValidator = function(value) { + return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value), value); + }; ctrl.$formatters.push(patternValidator); ctrl.$parsers.push(patternValidator); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index c026f9a455a9..4c2f62274485 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -423,6 +423,15 @@ describe('input', function() { scope.$digest(); } + var attrs; + beforeEach(module(function($compileProvider) { + $compileProvider.directive('attrCapture', function() { + return function(scope, element, $attrs) { + attrs = $attrs; + }; + }); + })); + beforeEach(inject(function($injector, _$sniffer_, _$browser_) { $sniffer = _$sniffer_; $browser = _$browser_; @@ -1073,6 +1082,19 @@ describe('input', function() { expect(inputElm).toBeInvalid(); }); + it('should listen on ng-pattern when pattern is observed', function() { + var value, patternVal = /^\w+$/; + compileInput(''); + attrs.$observe('pattern', function(v) { + value = attrs.pattern; + }); + + scope.$apply(function() { + scope.pat = patternVal; + }); + + expect(value).toBe(patternVal); + }); it('should validate in-lined pattern with modifiers', function() { compileInput(''); @@ -1104,7 +1126,9 @@ describe('input', function() { changeInputValueTo('x'); expect(inputElm).toBeInvalid(); - scope.regexp = /abc?/; + scope.$apply(function() { + scope.regexp = /abc?/; + }); changeInputValueTo('ab'); expect(inputElm).toBeValid(); @@ -1114,10 +1138,12 @@ describe('input', function() { }); - it('should throw an error when scope pattern can\'t be found', function() { + it('should throw an error when scope pattern is invalid', function() { expect(function() { compileInput(''); - scope.$apply(); + scope.$apply(function() { + scope.fooRegexp = '/...'; + }); }).toThrowMatching(/^\[ngPattern:noregexp\] Expected fooRegexp to be a RegExp but was/); }); }); @@ -1134,6 +1160,20 @@ describe('input', function() { changeInputValueTo('aaa'); expect(scope.value).toBe('aaa'); }); + + it('should listen on ng-minlength when minlength is observed', function() { + var value = 0; + compileInput(''); + attrs.$observe('minlength', function(v) { + value = int(attrs.minlength); + }); + + scope.$apply(function() { + scope.min = 5; + }); + + expect(value).toBe(5); + }); }); @@ -1148,6 +1188,20 @@ describe('input', function() { changeInputValueTo('aaa'); expect(scope.value).toBe('aaa'); }); + + it('should listen on ng-maxlength when maxlength is observed', function() { + var value = 0; + compileInput(''); + attrs.$observe('maxlength', function(v) { + value = int(attrs.maxlength); + }); + + scope.$apply(function() { + scope.max = 10; + }); + + expect(value).toBe(10); + }); }); diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 77abbd2dd7b0..a0d95b158910 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -953,7 +953,6 @@ describe('parser', function() { })); }); - describe('one-time binding', function() { it('should only use the cache when it is not a one-time binding', inject(function($parse) { expect($parse('foo')).toBe($parse('foo'));