Skip to content

Commit b9c338d

Browse files
committed
fix(jqLite): separate jqLite/compile/sce camelCasing logic
jqLite needs camelCase for it's css method; it should only convert one dash followed by a lowercase letter to an uppercase one; it shouldn't touch underscores, colons or collapse multiple dashes into one. This is behavior of jQuery 3 as well. This commit separates jqLite camelCasing from the $compile one (and $sce but that's an internal-only use). The $compile version should behave as before. Also, jqLite's css camelCasing logic was put in a separate function and refactored: now the properties starting from an uppercase letter are used by default (i.e. Webkit, not webkit) and the only exception is for the -ms- prefix that is converted to ms, not Ms. This makes the logic clearer as we're just always changing a dash followed by a lowercase letter by an uppercase one; this is also how it works in jQuery. Ref angular#15126 Fix angular#7744 BREAKING CHANGE: before, when Angular was used without jQuery, the key passed to the css method was more heavily camelCased; now only a single (!) hyphen followed by a lowercase letter is getting transformed. This also affects APIs that rely on the css method, like ngStyle. If you use Angular with jQuery, it already behaved in this way so no changes are needed on your part. To migrate the code follow the example below: Before: HTML: // All five versions used to be equivalent. <div ng-style={background_color: 'blue'}></div> <div ng-style={'background:color': 'blue'}></div> <div ng-style={'background-color': 'blue'}></div> <div ng-style={'background--color': 'blue'}></div> <div ng-style={backgroundColor: 'blue'}></div> JS: // All five versions used to be equivalent. elem.css('background_color', 'blue'); elem.css('background:color', 'blue'); elem.css('background-color', 'blue'); elem.css('background--color', 'blue'); elem.css('backgroundColor', 'blue'); // All five versions used to be equivalent. var bgColor = elem.css('background_color'); var bgColor = elem.css('background:color'); var bgColor = elem.css('background-color'); var bgColor = elem.css('background--color'); var bgColor = elem.css('backgroundColor'); After: HTML: // Previous five versions are no longer equivalent but these two still are. <div ng-style={'background-color': 'blue'}></div> <div ng-style={backgroundColor: 'blue'}></div> JS: // Previous five versions are no longer equivalent but these two still are. elem.css('background-color', 'blue'); elem.css('backgroundColor', 'blue'); // Previous five versions are no longer equivalent but these two still are. var bgColor = elem.css('background-color'); var bgColor = elem.css('backgroundColor');
1 parent beab3ba commit b9c338d

File tree

6 files changed

+159
-29
lines changed

6 files changed

+159
-29
lines changed

src/.eslintrc.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
"BOOLEAN_ATTR": false,
124124
"ALIASED_ATTR": false,
125125
"jqNextId": false,
126-
"camelCase": false,
126+
"fnCamelCaseReplace": false,
127127
"jqLitePatchJQueryRemove": false,
128128
"JQLite": false,
129129
"jqLiteClone": false,

src/jqLite.js

+20-11
Original file line numberDiff line numberDiff line change
@@ -136,22 +136,31 @@ JQLite._data = function(node) {
136136
function jqNextId() { return ++jqId; }
137137

138138

139-
var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g;
140-
var MOZ_HACK_REGEXP = /^moz([A-Z])/;
139+
var DASH_LOWERCASE_REGEXP = /-([a-z])/g;
140+
var MS_HACK_REGEXP = /^-ms-/;
141141
var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' };
142142
var jqLiteMinErr = minErr('jqLite');
143143

144144
/**
145-
* Converts snake_case to camelCase.
146-
* Also there is special case for Moz prefix starting with upper case letter.
145+
* Converts kebab-case to camelCase.
146+
* There is also a special case for the ms prefix starting with a lowercase letter.
147147
* @param name Name to normalize
148148
*/
149-
function camelCase(name) {
150-
return name.
151-
replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
152-
return offset ? letter.toUpperCase() : letter;
153-
}).
154-
replace(MOZ_HACK_REGEXP, 'Moz$1');
149+
function cssKebabToCamel(name) {
150+
return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-'));
151+
}
152+
153+
function fnCamelCaseReplace(all, letter) {
154+
return letter.toUpperCase();
155+
}
156+
157+
/**
158+
* Converts kebab-case to camelCase.
159+
* @param name Name to normalize
160+
*/
161+
function kebabToCamel(name) {
162+
return name
163+
.replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace);
155164
}
156165

157166
var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/;
@@ -633,7 +642,7 @@ forEach({
633642
hasClass: jqLiteHasClass,
634643

635644
css: function(element, name, value) {
636-
name = camelCase(name);
645+
name = cssKebabToCamel(name);
637646

638647
if (isDefined(value)) {
639648
element.style[name] = value;

src/ng/compile.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -3590,12 +3590,16 @@ SimpleChange.prototype.isFirstChange = function() { return this.previousValue ==
35903590

35913591

35923592
var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i;
3593+
var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g;
3594+
35933595
/**
35943596
* Converts all accepted directives format into proper directive name.
35953597
* @param name Name to normalize
35963598
*/
35973599
function directiveNormalize(name) {
3598-
return camelCase(name.replace(PREFIX_REGEXP, ''));
3600+
return name
3601+
.replace(PREFIX_REGEXP, '')
3602+
.replace(SPECIAL_CHARS_REGEXP, fnCamelCaseReplace);
35993603
}
36003604

36013605
/**

src/ng/sce.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ var SCE_CONTEXTS = {
2727

2828
// Helper functions follow.
2929

30+
var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g;
31+
32+
function snakeToCamel(name) {
33+
return name
34+
.replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace);
35+
}
36+
3037
function adjustMatcher(matcher) {
3138
if (matcher === 'self') {
3239
return matcher;
@@ -1054,13 +1061,13 @@ function $SceProvider() {
10541061

10551062
forEach(SCE_CONTEXTS, function(enumValue, name) {
10561063
var lName = lowercase(name);
1057-
sce[camelCase('parse_as_' + lName)] = function(expr) {
1064+
sce[snakeToCamel('parse_as_' + lName)] = function(expr) {
10581065
return parse(enumValue, expr);
10591066
};
1060-
sce[camelCase('get_trusted_' + lName)] = function(value) {
1067+
sce[snakeToCamel('get_trusted_' + lName)] = function(value) {
10611068
return getTrusted(enumValue, value);
10621069
};
1063-
sce[camelCase('trust_as_' + lName)] = function(value) {
1070+
sce[snakeToCamel('trust_as_' + lName)] = function(value) {
10641071
return trustAs(enumValue, value);
10651072
};
10661073
});

test/.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@
118118
/* jqLite.js */
119119
"BOOLEAN_ATTR": false,
120120
"jqNextId": false,
121-
"camelCase": false,
121+
"kebabToCamel": false,
122+
"fnCamelCaseReplace": false,
122123
"jqLitePatchJQueryRemove": false,
123124
"JQLite": false,
124125
"jqLiteClone": false,

test/jqLiteSpec.js

+121-12
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,105 @@ describe('jqLite', function() {
10551055
expect(jqA.css('z-index')).toBeOneOf('7', 7);
10561056
expect(jqA.css('zIndex')).toBeOneOf('7', 7);
10571057
});
1058+
1059+
it('should leave non-dashed strings alone', function() {
1060+
var jqA = jqLite(a);
1061+
1062+
jqA.css('foo', 'foo');
1063+
jqA.css('fooBar', 'bar');
1064+
1065+
expect(a.style.foo).toBe('foo');
1066+
expect(a.style.fooBar).toBe('bar');
1067+
});
1068+
1069+
it('should convert dash-separated strings to camelCase', function() {
1070+
var jqA = jqLite(a);
1071+
1072+
jqA.css('foo-bar', 'foo');
1073+
jqA.css('foo-bar-baz', 'bar');
1074+
jqA.css('foo:bar_baz', 'baz');
1075+
1076+
expect(a.style.fooBar).toBe('foo');
1077+
expect(a.style.fooBarBaz).toBe('bar');
1078+
expect(a.style['foo:bar_baz']).toBe('baz');
1079+
});
1080+
1081+
it('should convert leading dashes followed by a lowercase letter', function() {
1082+
var jqA = jqLite(a);
1083+
1084+
jqA.css('-foo-bar', 'foo');
1085+
1086+
expect(a.style.FooBar).toBe('foo');
1087+
});
1088+
1089+
it('should not convert slashes followed by a non-letter', function() {
1090+
// jQuery 2.x had different behavior; skip the test.
1091+
if (isJQuery2x()) return;
1092+
1093+
var jqA = jqLite(a);
1094+
1095+
jqA.css('foo-42- -a-B', 'foo');
1096+
1097+
expect(a.style['foo-42- A-B']).toBe('foo');
1098+
});
1099+
1100+
it('should convert the -ms- prefix to ms instead of Ms', function() {
1101+
var jqA = jqLite(a);
1102+
1103+
jqA.css('-ms-foo-bar', 'foo');
1104+
jqA.css('-moz-foo-bar', 'bar');
1105+
jqA.css('-webkit-foo-bar', 'baz');
1106+
1107+
expect(a.style.msFooBar).toBe('foo');
1108+
expect(a.style.MozFooBar).toBe('bar');
1109+
expect(a.style.WebkitFooBar).toBe('baz');
1110+
});
1111+
1112+
it('should not collapse sequences of dashes', function() {
1113+
var jqA = jqLite(a);
1114+
1115+
jqA.css('foo---bar-baz--qaz', 'foo');
1116+
1117+
expect(a.style['foo--BarBaz-Qaz']).toBe('foo');
1118+
});
1119+
1120+
1121+
it('should read vendor prefixes with the special -ms- exception', function() {
1122+
// jQuery uses getComputedStyle() in a css getter so these tests would fail there.
1123+
if (!_jqLiteMode) return;
1124+
1125+
var jqA = jqLite(a);
1126+
1127+
a.style.WebkitFooBar = 'webkit-uppercase';
1128+
a.style.webkitFooBar = 'webkit-lowercase';
1129+
1130+
a.style.MozFooBaz = 'moz-uppercase';
1131+
a.style.mozFooBaz = 'moz-lowercase';
1132+
1133+
a.style.MsFooQaz = 'ms-uppercase';
1134+
a.style.msFooQaz = 'ms-lowercase';
1135+
1136+
expect(jqA.css('-webkit-foo-bar')).toBe('webkit-uppercase');
1137+
expect(jqA.css('-moz-foo-baz')).toBe('moz-uppercase');
1138+
expect(jqA.css('-ms-foo-qaz')).toBe('ms-lowercase');
1139+
});
1140+
1141+
it('should write vendor prefixes with the special -ms- exception', function() {
1142+
var jqA = jqLite(a);
1143+
1144+
jqA.css('-webkit-foo-bar', 'webkit');
1145+
jqA.css('-moz-foo-baz', 'moz');
1146+
jqA.css('-ms-foo-qaz', 'ms');
1147+
1148+
expect(a.style.WebkitFooBar).toBe('webkit');
1149+
expect(a.style.webkitFooBar).not.toBeDefined();
1150+
1151+
expect(a.style.MozFooBaz).toBe('moz');
1152+
expect(a.style.mozFooBaz).not.toBeDefined();
1153+
1154+
expect(a.style.MsFooQaz).not.toBeDefined();
1155+
expect(a.style.msFooQaz).toBe('ms');
1156+
});
10581157
});
10591158

10601159

@@ -2267,25 +2366,35 @@ describe('jqLite', function() {
22672366
});
22682367

22692368

2270-
describe('camelCase', function() {
2369+
describe('kebabToCamel', function() {
22712370
it('should leave non-dashed strings alone', function() {
2272-
expect(camelCase('foo')).toBe('foo');
2273-
expect(camelCase('')).toBe('');
2274-
expect(camelCase('fooBar')).toBe('fooBar');
2371+
expect(kebabToCamel('foo')).toBe('foo');
2372+
expect(kebabToCamel('')).toBe('');
2373+
expect(kebabToCamel('fooBar')).toBe('fooBar');
2374+
});
2375+
2376+
it('should convert dash-separated strings to camelCase', function() {
2377+
expect(kebabToCamel('foo-bar')).toBe('fooBar');
2378+
expect(kebabToCamel('foo-bar-baz')).toBe('fooBarBaz');
2379+
expect(kebabToCamel('foo:bar_baz')).toBe('foo:bar_baz');
22752380
});
22762381

2382+
it('should convert leading dashes followed by a lowercase letter', function() {
2383+
expect(kebabToCamel('-foo-bar')).toBe('FooBar');
2384+
});
22772385

2278-
it('should covert dash-separated strings to camelCase', function() {
2279-
expect(camelCase('foo-bar')).toBe('fooBar');
2280-
expect(camelCase('foo-bar-baz')).toBe('fooBarBaz');
2281-
expect(camelCase('foo:bar_baz')).toBe('fooBarBaz');
2386+
it('should not convert dashes followed by a non-letter', function() {
2387+
expect(kebabToCamel('foo-42- -a-B')).toBe('foo-42- A-B');
22822388
});
22832389

2390+
it('should not convert browser specific css properties in a special way', function() {
2391+
expect(kebabToCamel('-ms-foo-bar')).toBe('MsFooBar');
2392+
expect(kebabToCamel('-moz-foo-bar')).toBe('MozFooBar');
2393+
expect(kebabToCamel('-webkit-foo-bar')).toBe('WebkitFooBar');
2394+
});
22842395

2285-
it('should covert browser specific css properties', function() {
2286-
expect(camelCase('-moz-foo-bar')).toBe('MozFooBar');
2287-
expect(camelCase('-webkit-foo-bar')).toBe('webkitFooBar');
2288-
expect(camelCase('-webkit-foo-bar')).toBe('webkitFooBar');
2396+
it('should not collapse sequences of dashes', function() {
2397+
expect(kebabToCamel('foo---bar-baz--qaz')).toBe('foo--BarBaz-Qaz');
22892398
});
22902399
});
22912400

0 commit comments

Comments
 (0)