Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

refactor(data): camelCase keys in jqLite#data #15238

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
"BOOLEAN_ATTR": false,
"ALIASED_ATTR": false,
"jqNextId": false,
"camelCase": false,
"fnCamelCaseReplace": false,
"jqLitePatchJQueryRemove": false,
"JQLite": false,
"jqLiteClone": false,
Expand Down
40 changes: 26 additions & 14 deletions src/jqLite.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,22 +136,31 @@ JQLite._data = function(node) {
function jqNextId() { return ++jqId; }


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

/**
* Converts snake_case to camelCase.
* Also there is special case for Moz prefix starting with upper case letter.
* Converts kebab-case to camelCase.
* There is also a special case for the ms prefix starting with a lowercase letter.
* @param name Name to normalize
*/
function camelCase(name) {
return name.
replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
return offset ? letter.toUpperCase() : letter;
}).
replace(MOZ_HACK_REGEXP, 'Moz$1');
function cssKebabToCamel(name) {
return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-'));
}

function fnCamelCaseReplace(all, letter) {
return letter.toUpperCase();
}

/**
* Converts kebab-case to camelCase.
* @param name Name to normalize
*/
function kebabToCamel(name) {
return name
.replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace);
}

var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/;
Expand Down Expand Up @@ -385,6 +394,7 @@ function jqLiteExpandoStore(element, createIfNecessary) {

function jqLiteData(element, key, value) {
if (jqLiteAcceptsData(element)) {
var prop;

var isSimpleSetter = isDefined(value);
var isSimpleGetter = !isSimpleSetter && key && !isObject(key);
Expand All @@ -393,16 +403,18 @@ function jqLiteData(element, key, value) {
var data = expandoStore && expandoStore.data;

if (isSimpleSetter) { // data('key', value)
data[key] = value;
data[kebabToCamel(key)] = value;
} else {
if (massGetter) { // data()
return data;
} else {
if (isSimpleGetter) { // data('key')
// don't force creation of expandoStore if it doesn't exist yet
return data && data[key];
return data && data[kebabToCamel(key)];
} else { // mass-setter: data({key1: val1, key2: val2})
extend(data, key);
for (prop in key) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is different than extend:

  1. extend only copies own properties.
  2. extend ignores $$hashKey.

If the chenges are intentional (e.g. to align with jQuery), we should clearly document them in the breaking change notice.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data[kebabToCamel(prop)] = key[prop];
}
}
}
}
Expand Down Expand Up @@ -633,7 +645,7 @@ forEach({
hasClass: jqLiteHasClass,

css: function(element, name, value) {
name = camelCase(name);
name = cssKebabToCamel(name);

if (isDefined(value)) {
element.style[name] = value;
Expand Down
6 changes: 5 additions & 1 deletion src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -3590,12 +3590,16 @@ SimpleChange.prototype.isFirstChange = function() { return this.previousValue ==


var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i;
var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g;

/**
* Converts all accepted directives format into proper directive name.
* @param name Name to normalize
*/
function directiveNormalize(name) {
return camelCase(name.replace(PREFIX_REGEXP, ''));
return name
.replace(PREFIX_REGEXP, '')
.replace(SPECIAL_CHARS_REGEXP, fnCamelCaseReplace);
}

/**
Expand Down
13 changes: 10 additions & 3 deletions src/ng/sce.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ var SCE_CONTEXTS = {

// Helper functions follow.

var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g;

function snakeToCamel(name) {
return name
.replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace);
}

function adjustMatcher(matcher) {
if (matcher === 'self') {
return matcher;
Expand Down Expand Up @@ -1054,13 +1061,13 @@ function $SceProvider() {

forEach(SCE_CONTEXTS, function(enumValue, name) {
var lName = lowercase(name);
sce[camelCase('parse_as_' + lName)] = function(expr) {
sce[snakeToCamel('parse_as_' + lName)] = function(expr) {
return parse(enumValue, expr);
};
sce[camelCase('get_trusted_' + lName)] = function(value) {
sce[snakeToCamel('get_trusted_' + lName)] = function(value) {
return getTrusted(enumValue, value);
};
sce[camelCase('trust_as_' + lName)] = function(value) {
sce[snakeToCamel('trust_as_' + lName)] = function(value) {
return trustAs(enumValue, value);
};
});
Expand Down
3 changes: 2 additions & 1 deletion test/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@
/* jqLite.js */
"BOOLEAN_ATTR": false,
"jqNextId": false,
"camelCase": false,
"kebabToCamel": false,
"fnCamelCaseReplace": false,
"jqLitePatchJQueryRemove": false,
"JQLite": false,
"jqLiteClone": false,
Expand Down
166 changes: 154 additions & 12 deletions test/jqLiteSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,39 @@ describe('jqLite', function() {
}).not.toThrow();
});
});

describe('camelCasing keys', function() {
// jQuery 2.x has different behavior; skip the tests.
if (isJQuery2x()) return;

it('should camelCase the key in a setter', function() {
var element = jqLite(a);

element.data('a-B-c-d-42--e', 'z-x');
expect(element.data()).toEqual({'a-BCD-42-E': 'z-x'});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this actually how we want the camelcasing behaviour to work when there are already capital letters and double hyphens in the key? Or is it because of our current implementation of camelCase()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly as jQuery does it. Only a single dash + a lowercase letter get converted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also how the HTML5 spec converts data attribute names to keys in dataset. That's where jQuery took it from.

It's also nice in that it's fully 1-1, the conversion doesn't lose any information.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

});

it('should camelCase the key in a getter', function() {
var element = jqLite(a);

element.data()['a-BCD-42-E'] = 'x-c';
expect(element.data('a-B-c-d-42--e')).toBe('x-c');
});

it('should camelCase the key in a mass setter', function() {
var element = jqLite(a);

element.data({'a-B-c-d-42--e': 'c-v', 'r-t-v': 42});
expect(element.data()).toEqual({'a-BCD-42-E': 'c-v', 'rTV': 42});
});

it('should ignore non-camelCase keys in the data in a getter', function() {
var element = jqLite(a);

element.data()['a-b'] = 'b-n';
expect(element.data('a-b')).toBe(undefined);
});
});
});


Expand Down Expand Up @@ -1055,6 +1088,105 @@ describe('jqLite', function() {
expect(jqA.css('z-index')).toBeOneOf('7', 7);
expect(jqA.css('zIndex')).toBeOneOf('7', 7);
});

it('should leave non-dashed strings alone', function() {
var jqA = jqLite(a);

jqA.css('foo', 'foo');
jqA.css('fooBar', 'bar');

expect(a.style.foo).toBe('foo');
expect(a.style.fooBar).toBe('bar');
});

it('should convert dash-separated strings to camelCase', function() {
var jqA = jqLite(a);

jqA.css('foo-bar', 'foo');
jqA.css('foo-bar-baz', 'bar');
jqA.css('foo:bar_baz', 'baz');

expect(a.style.fooBar).toBe('foo');
expect(a.style.fooBarBaz).toBe('bar');
expect(a.style['foo:bar_baz']).toBe('baz');
});

it('should convert leading dashes followed by a lowercase letter', function() {
var jqA = jqLite(a);

jqA.css('-foo-bar', 'foo');

expect(a.style.FooBar).toBe('foo');
});

it('should not convert slashes followed by a non-letter', function() {
// jQuery 2.x had different behavior; skip the test.
if (isJQuery2x()) return;

var jqA = jqLite(a);

jqA.css('foo-42- -a-B', 'foo');

expect(a.style['foo-42- A-B']).toBe('foo');
});

it('should convert the -ms- prefix to ms instead of Ms', function() {
var jqA = jqLite(a);

jqA.css('-ms-foo-bar', 'foo');
jqA.css('-moz-foo-bar', 'bar');
jqA.css('-webkit-foo-bar', 'baz');

expect(a.style.msFooBar).toBe('foo');
expect(a.style.MozFooBar).toBe('bar');
expect(a.style.WebkitFooBar).toBe('baz');
});

it('should not collapse sequences of dashes', function() {
var jqA = jqLite(a);

jqA.css('foo---bar-baz--qaz', 'foo');

expect(a.style['foo--BarBaz-Qaz']).toBe('foo');
});


it('should read vendor prefixes with the special -ms- exception', function() {
// jQuery uses getComputedStyle() in a css getter so these tests would fail there.
if (!_jqLiteMode) return;

var jqA = jqLite(a);

a.style.WebkitFooBar = 'webkit-uppercase';
a.style.webkitFooBar = 'webkit-lowercase';

a.style.MozFooBaz = 'moz-uppercase';
a.style.mozFooBaz = 'moz-lowercase';

a.style.MsFooQaz = 'ms-uppercase';
a.style.msFooQaz = 'ms-lowercase';

expect(jqA.css('-webkit-foo-bar')).toBe('webkit-uppercase');
expect(jqA.css('-moz-foo-baz')).toBe('moz-uppercase');
expect(jqA.css('-ms-foo-qaz')).toBe('ms-lowercase');
});

it('should write vendor prefixes with the special -ms- exception', function() {
var jqA = jqLite(a);

jqA.css('-webkit-foo-bar', 'webkit');
jqA.css('-moz-foo-baz', 'moz');
jqA.css('-ms-foo-qaz', 'ms');

expect(a.style.WebkitFooBar).toBe('webkit');
expect(a.style.webkitFooBar).not.toBeDefined();

expect(a.style.MozFooBaz).toBe('moz');
expect(a.style.mozFooBaz).not.toBeDefined();

expect(a.style.MsFooQaz).not.toBeDefined();
expect(a.style.msFooQaz).toBe('ms');
});
});


Expand Down Expand Up @@ -2267,25 +2399,35 @@ describe('jqLite', function() {
});


describe('camelCase', function() {
describe('kebabToCamel', function() {
it('should leave non-dashed strings alone', function() {
expect(camelCase('foo')).toBe('foo');
expect(camelCase('')).toBe('');
expect(camelCase('fooBar')).toBe('fooBar');
expect(kebabToCamel('foo')).toBe('foo');
expect(kebabToCamel('')).toBe('');
expect(kebabToCamel('fooBar')).toBe('fooBar');
});

it('should convert dash-separated strings to camelCase', function() {
expect(kebabToCamel('foo-bar')).toBe('fooBar');
expect(kebabToCamel('foo-bar-baz')).toBe('fooBarBaz');
expect(kebabToCamel('foo:bar_baz')).toBe('foo:bar_baz');
});

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

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

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

it('should covert browser specific css properties', function() {
expect(camelCase('-moz-foo-bar')).toBe('MozFooBar');
expect(camelCase('-webkit-foo-bar')).toBe('webkitFooBar');
expect(camelCase('-webkit-foo-bar')).toBe('webkitFooBar');
it('should not collapse sequences of dashes', function() {
expect(kebabToCamel('foo---bar-baz--qaz')).toBe('foo--BarBaz-Qaz');
});
});

Expand Down