Skip to content

Commit

Permalink
Merge pull request #438 from capricorn86/task/405-assigned-keys-to-ht…
Browse files Browse the repository at this point in the history
…mlelementdataset-are-ignored

Task/405 assigned keys to htmlelementdataset are ignored
  • Loading branch information
capricorn86 committed Mar 31, 2022
2 parents 7f23542 + cff6a6b commit a464572
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 13 deletions.
7 changes: 7 additions & 0 deletions packages/happy-dom/src/nodes/element/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,13 @@ export default class Element extends Node implements IElement {
*/
public getAttributeNodeNS(namespace: string, name: string): Attr {
const attributeName = this._getAttributeName(name);
if (
this._attributes[attributeName] &&
this._attributes[attributeName].namespaceURI === namespace &&
this._attributes[attributeName].localName === attributeName
) {
return this._attributes[attributeName];
}
for (const name of Object.keys(this._attributes)) {
const attribute = this._attributes[name];
if (attribute.namespaceURI === namespace && attribute.localName === attributeName) {
Expand Down
28 changes: 28 additions & 0 deletions packages/happy-dom/src/nodes/html-element/DatasetUtility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Dataset utility.
*/
export default class DatasetUtility {
/**
* Transforms a kebab cased string to camel case.
*
* @param text Text string.
* @returns Camel cased string.
*/
public static kebabToCamelCase(text: string): string {
const parts = text.split('-');
for (let i = 0, max = parts.length; i < max; i++) {
parts[i] = i > 0 ? parts[i].charAt(0).toUpperCase() + parts[i].slice(1) : parts[i];
}
return parts.join('');
}

/**
* Transforms a camel cased string to kebab case.
*
* @param text Text string.
* @returns Kebab cased string.
*/
public static camelCaseToKebab(text: string): string {
return text.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? '-' : '') + $.toLowerCase());
}
}
70 changes: 66 additions & 4 deletions packages/happy-dom/src/nodes/html-element/HTMLElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Attr from '../../attribute/Attr';
import FocusEvent from '../../event/events/FocusEvent';
import PointerEvent from '../../event/events/PointerEvent';
import Node from '../node/Node';
import DatasetUtility from './DatasetUtility';

/**
* HTML Element.
Expand All @@ -25,6 +26,7 @@ export default class HTMLElement extends Element implements IHTMLElement {
public readonly clientWidth = 0;

private _style: CSSStyleDeclaration = null;
private _dataset: { [key: string]: string } = null;

/**
* Returns tab index.
Expand Down Expand Up @@ -98,13 +100,73 @@ export default class HTMLElement extends Element implements IHTMLElement {
* @returns Data set.
*/
public get dataset(): { [key: string]: string } {
const dataset = {};
for (const name of Object.keys(this._attributes)) {
if (this._dataset) {
return this._dataset;
}

const dataset: { [key: string]: string } = {};
const attributes = this._attributes;

for (const name of Object.keys(attributes)) {
if (name.startsWith('data-')) {
dataset[name.replace('data-', '')] = this._attributes[name].value;
const key = DatasetUtility.kebabToCamelCase(name.replace('data-', ''));
dataset[key] = attributes[name].value;
}
}
return dataset;

// Documentation for Proxy:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
this._dataset = new Proxy(dataset, {
get: (dataset: { [key: string]: string }, key: string): string => {
const name = 'data-' + DatasetUtility.camelCaseToKebab(key);
if (this._attributes[name]) {
dataset[key] = this._attributes[name].value;
return this._attributes[name].value;
}
if (dataset[key] !== undefined) {
delete dataset[key];
}
return undefined;
},
set: (dataset: { [key: string]: string }, key: string, value: string): boolean => {
this.setAttribute('data-' + DatasetUtility.camelCaseToKebab(key), value);
dataset[key] = value;
return true;
},
deleteProperty: (dataset: { [key: string]: string }, key: string) => {
const name = 'data-' + DatasetUtility.camelCaseToKebab(key);
const exists = !!attributes[name];
delete attributes[name];
delete dataset[key];
return exists;
},
ownKeys: (dataset: { [key: string]: string }) => {
// According to Mozilla we have to update the dataset object (target) to contain the same keys as what we return:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys
// "The result List must contain the keys of all non-configurable own properties of the target object."
const keys = [];
const deleteKeys = [];
for (const name of Object.keys(attributes)) {
if (name.startsWith('data-')) {
const key = DatasetUtility.kebabToCamelCase(name.replace('data-', ''));
keys.push(key);
dataset[key] = attributes[name].value;
if (!dataset[key]) {
deleteKeys.push(key);
}
}
}
for (const key of deleteKeys) {
delete dataset[key];
}
return keys;
},
has: (_dataset: { [key: string]: string }, key: string) => {
return !!attributes['data-' + DatasetUtility.camelCaseToKebab(key)];
}
});

return this._dataset;
}

/**
Expand Down
38 changes: 29 additions & 9 deletions packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,35 @@ describe('HTMLElement', () => {
});

describe('get dataset()', () => {
it('Returns attributes prefixed with "data-" as an object.', () => {
element.setAttribute('test1', 'value1');
element.setAttribute('data-test2', 'value2');
element.setAttribute('test3', 'value3');
element.setAttribute('data-test4', 'value4');
expect(element.dataset).toEqual({
test2: 'value2',
test4: 'value4'
});
it('Returns a Proxy behaving like an object that can add, remove, set and get element attributes prefixed with "data-".', () => {
element.setAttribute('test-alpha', 'value1');
element.setAttribute('data-test-alpha', 'value2');
element.setAttribute('test-beta', 'value3');
element.setAttribute('data-test-beta', 'value4');

const dataset = element.dataset;

expect(dataset).toBe(element.dataset);
expect(Object.keys(dataset)).toEqual(['testAlpha', 'testBeta']);
expect(Object.values(dataset)).toEqual(['value2', 'value4']);

dataset.testGamma = 'value5';

expect(element.getAttribute('data-test-gamma')).toBe('value5');
expect(Object.keys(dataset)).toEqual(['testAlpha', 'testBeta', 'testGamma']);
expect(Object.values(dataset)).toEqual(['value2', 'value4', 'value5']);

element.setAttribute('data-test-delta', 'value6');

expect(dataset.testDelta).toBe('value6');
expect(Object.keys(dataset)).toEqual(['testAlpha', 'testBeta', 'testGamma', 'testDelta']);
expect(Object.values(dataset)).toEqual(['value2', 'value4', 'value5', 'value6']);

delete dataset.testDelta;

expect(element.getAttribute('data-test-delta')).toBe(null);
expect(Object.keys(dataset)).toEqual(['testAlpha', 'testBeta', 'testGamma']);
expect(Object.values(dataset)).toEqual(['value2', 'value4', 'value5']);
});
});

Expand Down

0 comments on commit a464572

Please sign in to comment.