Skip to content

Commit 83ae08f

Browse files
committed
feat(parser): automatically observe aliased ng- attributes and natively instantiate regular expressions
Closes angular#7758
1 parent 3df2cca commit 83ae08f

11 files changed

+144
-33
lines changed

Gruntfile.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ module.exports = function(grunt) {
234234
'src/**/*.js',
235235
'test/**/*.js',
236236
'!test/ngScenario/DescribeSpec.js',
237-
'!src/ng/directive/booleanAttrs.js', // legitimate xit here
237+
'!src/ng/directive/attrs.js', // legitimate xit here
238238
'!src/ngScenario/**/*.js'
239239
]
240240
},

angularFiles.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ angularFiles = {
4444

4545
'src/ng/directive/directives.js',
4646
'src/ng/directive/a.js',
47-
'src/ng/directive/booleanAttrs.js',
47+
'src/ng/directive/attrs.js',
4848
'src/ng/directive/form.js',
4949
'src/ng/directive/input.js',
5050
'src/ng/directive/ngBind.js',

src/.jshintrc

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"nodeName_": false,
3535
"uid": false,
3636

37+
"REGEX_STRING_REGEXP" : false,
3738
"lowercase": false,
3839
"uppercase": false,
3940
"manualLowercase": false,
@@ -117,6 +118,7 @@
117118

118119
/* jqLite.js */
119120
"BOOLEAN_ATTR": false,
121+
"ALIASED_ATTR": false,
120122
"jqNextId": false,
121123
"camelCase": false,
122124
"jqLitePatchJQueryRemove": false,
@@ -134,6 +136,7 @@
134136
"jqLiteController": false,
135137
"jqLiteInheritedData": false,
136138
"getBooleanAttrName": false,
139+
"getAliasedAttrName": false,
137140
"createEventHandler": false,
138141
"JQLitePrototype": false,
139142
"addEventListenerFn": false,

src/Angular.js

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
-angularModule,
1414
-nodeName_,
1515
-uid,
16+
-REGEX_STRING_REGEXP,
1617
1718
-lowercase,
1819
-uppercase,
@@ -102,6 +103,8 @@
102103
* <div doc-module-components="ng"></div>
103104
*/
104105

106+
var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;
107+
105108
/**
106109
* @ngdoc function
107110
* @name angular.lowercase

src/jqLite.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
-JQLitePrototype,
66
-addEventListenerFn,
77
-removeEventListenerFn,
8-
-BOOLEAN_ATTR
8+
-BOOLEAN_ATTR,
9+
-ALIASED_ATTR
910
*/
1011

1112
//////////////////////////////////
@@ -463,6 +464,11 @@ var BOOLEAN_ELEMENTS = {};
463464
forEach('input,select,option,textarea,button,form,details'.split(','), function(value) {
464465
BOOLEAN_ELEMENTS[uppercase(value)] = true;
465466
});
467+
var ALIASED_ATTR = {
468+
'ngMinlength' : 'minlength',
469+
'ngMaxlength' : 'maxlength',
470+
'ngPattern' : 'pattern'
471+
};
466472

467473
function getBooleanAttrName(element, name) {
468474
// check dom last since we will most likely fail on name
@@ -472,6 +478,11 @@ function getBooleanAttrName(element, name) {
472478
return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr;
473479
}
474480

481+
function getAliasedAttrName(element, name) {
482+
var nodeName = element.nodeName;
483+
return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name];
484+
}
485+
475486
forEach({
476487
data: jqLiteData,
477488
inheritedData: jqLiteInheritedData,

src/ng/compile.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -729,13 +729,19 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
729729
//is set through this function since it may cause $updateClass to
730730
//become unstable.
731731

732-
var booleanKey = getBooleanAttrName(this.$$element[0], key),
732+
var node = this.$$element[0],
733+
booleanKey = getBooleanAttrName(node, key),
734+
aliasedKey = getAliasedAttrName(node, key),
735+
observer = key,
733736
normalizedVal,
734737
nodeName;
735738

736739
if (booleanKey) {
737740
this.$$element.prop(key, value);
738741
attrName = booleanKey;
742+
} else if(aliasedKey) {
743+
this[aliasedKey] = value;
744+
observer = aliasedKey;
739745
}
740746

741747
this[key] = value;
@@ -768,7 +774,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
768774

769775
// fire observers
770776
var $$observers = this.$$observers;
771-
$$observers && forEach($$observers[key], function(fn) {
777+
$$observers && forEach($$observers[observer], function(fn) {
772778
try {
773779
fn(value);
774780
} catch (e) {

src/ng/directive/booleanAttrs.js src/ng/directive/attrs.js

+13
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,19 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) {
361361
};
362362
});
363363

364+
// aliased input attrs are evaluated
365+
forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
366+
ngAttributeAliasDirectives[ngAttr] = function() {
367+
return {
368+
priority: 100,
369+
link: function(scope, element, attr) {
370+
scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) {
371+
attr.$set(ngAttr, value);
372+
});
373+
}
374+
};
375+
};
376+
});
364377

365378
// ng-src, ng-srcset, ng-href are interpolated
366379
forEach(['src', 'srcset', 'href'], function(attrName) {

src/ng/directive/input.js

+20-24
Original file line numberDiff line numberDiff line change
@@ -975,32 +975,28 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
975975
};
976976

977977
// pattern validator
978-
var pattern = attr.ngPattern,
979-
patternValidator,
980-
match;
978+
if (attr.ngPattern) {
979+
var regexp, patternExp = attr.ngPattern;
980+
attr.$observe('pattern', function(regex) {
981+
if(isString(regex)) {
982+
var match = regex.match(REGEX_STRING_REGEXP);
983+
if(match) {
984+
regex = new RegExp(match[1], match[2]);
985+
}
986+
}
981987

982-
if (pattern) {
983-
var validateRegex = function(regexp, value) {
984-
return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value);
985-
};
986-
match = pattern.match(/^\/(.*)\/([gim]*)$/);
987-
if (match) {
988-
pattern = new RegExp(match[1], match[2]);
989-
patternValidator = function(value) {
990-
return validateRegex(pattern, value);
991-
};
992-
} else {
993-
patternValidator = function(value) {
994-
var patternObj = scope.$eval(pattern);
988+
if (regex && !regex.test) {
989+
throw minErr('ngPattern')('noregexp',
990+
'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
991+
regex, startingTag(element));
992+
}
995993

996-
if (!patternObj || !patternObj.test) {
997-
throw minErr('ngPattern')('noregexp',
998-
'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern,
999-
patternObj, startingTag(element));
1000-
}
1001-
return validateRegex(patternObj, value);
1002-
};
1003-
}
994+
regexp = regex || undefined;
995+
});
996+
997+
var patternValidator = function(value) {
998+
return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value), value);
999+
};
10041000

10051001
ctrl.$formatters.push(patternValidator);
10061002
ctrl.$parsers.push(patternValidator);

src/ng/parse.js

+13
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,19 @@ function $ParseProvider() {
10081008

10091009
exp = trim(exp);
10101010

1011+
if (exp.charAt(0) === '/') {
1012+
var match = exp.match(REGEX_STRING_REGEXP);
1013+
if (match) {
1014+
var regexp = new RegExp(match[1], match[2]);
1015+
var regexpWrapperFn = function() {
1016+
return regexp;
1017+
};
1018+
regexpWrapperFn.constant = true;
1019+
regexpWrapperFn.literal = false;
1020+
return regexpWrapperFn;
1021+
}
1022+
}
1023+
10111024
if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
10121025
oneTime = true;
10131026
exp = exp.substring(2);

test/ng/directive/inputSpec.js

+58-4
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,15 @@ describe('input', function() {
423423
scope.$digest();
424424
}
425425

426+
var attrs;
427+
beforeEach(module(function($compileProvider) {
428+
$compileProvider.directive('attrCapture', function() {
429+
return function(scope, element, $attrs) {
430+
attrs = $attrs;
431+
};
432+
});
433+
}));
434+
426435
beforeEach(inject(function($injector, _$sniffer_, _$browser_) {
427436
$sniffer = _$sniffer_;
428437
$browser = _$browser_;
@@ -1073,9 +1082,22 @@ describe('input', function() {
10731082
expect(inputElm).toBeInvalid();
10741083
});
10751084

1085+
it('should listen on ng-pattern when pattern is observed', function() {
1086+
var value, patternVal = /^\w+$/;
1087+
compileInput('<input type="text" ng-model="value" ng-pattern="pat" attr-capture />');
1088+
attrs.$observe('pattern', function(v) {
1089+
value = attrs.pattern;
1090+
});
1091+
1092+
scope.$apply(function() {
1093+
scope.pat = patternVal;
1094+
});
1095+
1096+
expect(value).toBe(patternVal);
1097+
});
10761098

10771099
it('should validate in-lined pattern with modifiers', function() {
1078-
compileInput('<input type="text" ng-model="value" ng-pattern="/^abc?$/i" />');
1100+
compileInput('<input type="text" ng-model="value" ng-pattern="\'/^abc?$/i\'" />');
10791101

10801102
changeInputValueTo('aB');
10811103
expect(inputElm).toBeValid();
@@ -1104,7 +1126,9 @@ describe('input', function() {
11041126
changeInputValueTo('x');
11051127
expect(inputElm).toBeInvalid();
11061128

1107-
scope.regexp = /abc?/;
1129+
scope.$apply(function() {
1130+
scope.regexp = /abc?/;
1131+
});
11081132

11091133
changeInputValueTo('ab');
11101134
expect(inputElm).toBeValid();
@@ -1114,10 +1138,12 @@ describe('input', function() {
11141138
});
11151139

11161140

1117-
it('should throw an error when scope pattern can\'t be found', function() {
1141+
it('should throw an error when scope pattern is invalid', function() {
11181142
expect(function() {
11191143
compileInput('<input type="text" ng-model="foo" ng-pattern="fooRegexp" />');
1120-
scope.$apply();
1144+
scope.$apply(function() {
1145+
scope.fooRegexp = '/...';
1146+
});
11211147
}).toThrowMatching(/^\[ngPattern:noregexp\] Expected fooRegexp to be a RegExp but was/);
11221148
});
11231149
});
@@ -1134,6 +1160,20 @@ describe('input', function() {
11341160
changeInputValueTo('aaa');
11351161
expect(scope.value).toBe('aaa');
11361162
});
1163+
1164+
it('should listen on ng-minlength when minlength is observed', function() {
1165+
var value = 0;
1166+
compileInput('<input type="text" ng-model="value" ng-minlength="min" attr-capture />');
1167+
attrs.$observe('minlength', function(v) {
1168+
value = int(attrs.minlength);
1169+
});
1170+
1171+
scope.$apply(function() {
1172+
scope.min = 5;
1173+
});
1174+
1175+
expect(value).toBe(5);
1176+
});
11371177
});
11381178

11391179

@@ -1148,6 +1188,20 @@ describe('input', function() {
11481188
changeInputValueTo('aaa');
11491189
expect(scope.value).toBe('aaa');
11501190
});
1191+
1192+
it('should listen on ng-maxlength when maxlength is observed', function() {
1193+
var value = 0;
1194+
compileInput('<input type="text" ng-model="value" ng-maxlength="max" attr-capture />');
1195+
attrs.$observe('maxlength', function(v) {
1196+
value = int(attrs.maxlength);
1197+
});
1198+
1199+
scope.$apply(function() {
1200+
scope.max = 10;
1201+
});
1202+
1203+
expect(value).toBe(10);
1204+
});
11511205
});
11521206

11531207

test/ng/parseSpec.js

+12
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,18 @@ describe('parser', function() {
953953
}));
954954
});
955955

956+
describe('regular expressions', function() {
957+
it('should parse and construct regular expression strings as regular expression objects',
958+
inject(function($parse) {
959+
960+
expect($parse('/^\\d+$/')()).toEqual(/^\d+$/);
961+
}));
962+
963+
it('should consider flags for regexp values', inject(function($parse) {
964+
var exp = $parse('/[aeiou]+/gi')();
965+
expect("annA".replace(exp, '')).toBe("nn");
966+
}));
967+
});
956968

957969
describe('one-time binding', function() {
958970
it('should only use the cache when it is not a one-time binding', inject(function($parse) {

0 commit comments

Comments
 (0)