Skip to content

Commit

Permalink
Merge pull request #1298 from capricorn86/1258-error-creating-custom-…
Browse files Browse the repository at this point in the history
…element

fix: [#1258] Adds support for constructing custom element using new k…
  • Loading branch information
capricorn86 committed Mar 12, 2024
2 parents f6e8467 + 1f21076 commit 0dfe51d
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 4 deletions.
49 changes: 49 additions & 0 deletions packages/happy-dom/src/custom-element/CustomElementRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import DOMException from '../exception/DOMException.js';
import * as PropertySymbol from '../PropertySymbol.js';
import HTMLElement from '../nodes/html-element/HTMLElement.js';
import Node from '../nodes/node/Node.js';
import IBrowserWindow from '../window/IBrowserWindow.js';
import NamespaceURI from '../config/NamespaceURI.js';

/**
* Custom elements registry.
Expand All @@ -12,6 +14,16 @@ export default class CustomElementRegistry {
} = {};
public [PropertySymbol.registedClass]: Map<typeof HTMLElement, string> = new Map();
public [PropertySymbol.callbacks]: { [k: string]: (() => void)[] } = {};
#window: IBrowserWindow;

/**
* Constructor.
*
* @param window Window.
*/
constructor(window: IBrowserWindow) {
this.#window = window;
}

/**
* Defines a custom element class.
Expand Down Expand Up @@ -44,6 +56,31 @@ export default class CustomElementRegistry {
);
}

const tagName = name.toUpperCase();

elementClass[PropertySymbol.ownerDocument] = this.#window.document;

Object.defineProperty(elementClass.prototype, 'localName', {
configurable: true,
get: function () {
return this[PropertySymbol.localName] || name;
}
});

Object.defineProperty(elementClass.prototype, 'tagName', {
configurable: true,
get: function () {
return this[PropertySymbol.tagName] || tagName;
}
});

Object.defineProperty(elementClass.prototype, 'namespaceURI', {
configurable: true,
get: function () {
return this[PropertySymbol.namespaceURI] || NamespaceURI.html;
}
});

this[PropertySymbol.registry][name] = {
elementClass,
extends: options && options.extends ? options.extends.toLowerCase() : null
Expand Down Expand Up @@ -113,6 +150,18 @@ export default class CustomElementRegistry {
return this[PropertySymbol.registedClass].get(elementClass) || null;
}

/**
* Destroys the registry.
*/
public [PropertySymbol.destroy](): void {
for (const entity of Object.values(this[PropertySymbol.registry])) {
entity.elementClass[PropertySymbol.ownerDocument] = null;
}
this[PropertySymbol.registry] = {};
this[PropertySymbol.registedClass] = new Map();
this[PropertySymbol.callbacks] = {};
}

/**
* Validates the correctness of custom element tag names.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-dom/src/nodes/document/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1122,7 +1122,7 @@ export default class Document extends Node implements IDocument {
];

if (customElement) {
const element = NodeFactory.createNode<IHTMLElement>(this, customElement.elementClass);
const element = new customElement.elementClass();
element[PropertySymbol.tagName] = qualifiedName.toUpperCase();
element[PropertySymbol.localName] = qualifiedName;
element[PropertySymbol.namespaceURI] = namespaceURI;
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-dom/src/nodes/node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export default class Node extends EventTarget implements INode {
* @param otherNode Node to test with.
* @returns "true" if this node contains the other node.
*/
public contains(otherNode: INode | undefined): boolean {
public contains(otherNode: INode): boolean {
if (otherNode === undefined) {
return false;
}
Expand Down
6 changes: 5 additions & 1 deletion packages/happy-dom/src/window/BrowserWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow

this.#browserFrame = browserFrame;

this.customElements = new CustomElementRegistry();
this.customElements = new CustomElementRegistry(this);
this.navigator = new Navigator(this);
this.history = new History();
this.screen = new Screen();
Expand Down Expand Up @@ -1274,6 +1274,10 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow
this.document.removeChild(node);
}

if (this.customElements[PropertySymbol.destroy]) {
this.customElements[PropertySymbol.destroy]();
}

this.document[PropertySymbol.activeElement] = null;
this.document[PropertySymbol.nextActiveElement] = null;
this.document[PropertySymbol.currentScript] = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Window from '../../src/window/Window.js';
import DOMException from '../../src/exception/DOMException.js';
import { beforeEach, describe, it, expect } from 'vitest';
import * as PropertySymbol from '../../src/PropertySymbol.js';
import NamespaceURI from '../../src/config/NamespaceURI.js';

describe('CustomElementRegistry', () => {
let customElements;
Expand All @@ -15,7 +16,7 @@ describe('CustomElementRegistry', () => {
beforeEach(() => {
window = new Window();
document = window.document;
customElements = new CustomElementRegistry();
customElements = new CustomElementRegistry(window);
CustomElement.observedAttributesCallCount = 0;
});

Expand All @@ -37,6 +38,16 @@ describe('CustomElementRegistry', () => {
expect(customElements[PropertySymbol.registry]['custom-element'].extends).toBe('ul');
});

it('Can construct CustomElement instance using "new".', () => {
customElements.define('custom-element', CustomElement);
const customElement = new CustomElement();
expect(customElement).toBeInstanceOf(CustomElement);
expect(customElement.ownerDocument).toBe(document);
expect(customElement.localName).toBe('custom-element');
expect(customElement.tagName).toBe('CUSTOM-ELEMENT');
expect(customElement.namespaceURI).toBe(NamespaceURI.html);
});

it('Throws an error if tag name does not contain "-".', () => {
expect(() => customElements.define('element', CustomElement)).toThrow(
new DOMException(
Expand Down

0 comments on commit 0dfe51d

Please sign in to comment.