Skip to content

Commit

Permalink
#668@patch: Fixes issue where all properties didn't get copied from t…
Browse files Browse the repository at this point in the history
…he HTMLUnknownElement that gets replaced by a custom element when it is defined by calling CustomElementRegistry.define() after it is used.
  • Loading branch information
capricorn86 committed May 23, 2023
1 parent 6b49cc3 commit 556ee53
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 16 deletions.
6 changes: 4 additions & 2 deletions packages/happy-dom/src/nodes/document/Document.ts
Expand Up @@ -47,6 +47,7 @@ import ProcessingInstruction from '../processing-instruction/ProcessingInstructi
import ElementUtility from '../element/ElementUtility';
import HTMLCollection from '../element/HTMLCollection';
import VisibilityStateEnum from './VisibilityStateEnum';
import NodeTypeEnum from '../node/NodeTypeEnum';

const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/;

Expand Down Expand Up @@ -678,7 +679,7 @@ export default class Document extends Node implements IDocument {
for (const node of root.childNodes) {
if (node['tagName'] === 'HTML') {
documentElement = node;
} else if (node.nodeType === Node.DOCUMENT_TYPE_NODE) {
} else if (node.nodeType === NodeTypeEnum.documentTypeNode) {
documentTypeNode = node;
}

Expand All @@ -704,10 +705,11 @@ export default class Document extends Node implements IDocument {
}
}

// Remaining nodes outside the <html> element are added to the <body> element.
const body = ParentNodeUtility.getElementByTagName(this, 'body');
if (body) {
for (const child of root.childNodes.slice()) {
if (child['tagName'] !== 'HTML' && child.nodeType !== Node.DOCUMENT_TYPE_NODE) {
if (child['tagName'] !== 'HTML' && child.nodeType !== NodeTypeEnum.documentTypeNode) {
body.appendChild(child);
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-dom/src/nodes/element/Element.ts
Expand Up @@ -92,7 +92,7 @@ export default class Element extends Node implements IElement {
public _attributes: { [k: string]: IAttr } = {};

private _classList: DOMTokenList = null;
public _isValue?: string;
public _isValue?: string | null = null;
public _computedStyle: CSSStyleDeclaration | null = null;

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/happy-dom/src/nodes/html-element/HTMLElement.ts
Expand Up @@ -27,8 +27,8 @@ export default class HTMLElement extends Element implements IHTMLElement {
public readonly clientHeight = 0;
public readonly clientWidth = 0;

private _style: CSSStyleDeclaration = null;
private _dataset: Dataset = null;
public _style: CSSStyleDeclaration = null;
public _dataset: Dataset = null;

// Events
public oncopy: (event: Event) => void | null = null;
Expand Down
@@ -1,6 +1,11 @@
import HTMLElement from '../html-element/HTMLElement';
import INode from '../node/INode';
import IHTMLElement from '../html-element/IHTMLElement';
import INodeList from '../node/INodeList';
import IHTMLCollection from '../element/IHTMLCollection';
import IElement from '../element/IElement';
import NodeList from '../node/NodeList';
import HTMLCollection from '../element/HTMLCollection';

/**
* HTML Unknown Element.
Expand All @@ -27,9 +32,48 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem
if (parentNode && !this._customElementDefineCallback) {
const callback = (): void => {
if (this.parentNode) {
const newElement = this.ownerDocument.createElement(tagName);
this.parentNode.insertBefore(newElement, this);
this.parentNode.removeChild(this);
const newElement = <HTMLElement>this.ownerDocument.createElement(tagName);
(<INodeList<INode>>newElement.childNodes) = this.childNodes;
(<IHTMLCollection<IElement>>newElement.children) = this.children;
(<boolean>newElement.isConnected) = this.isConnected;

newElement._rootNode = this._rootNode;
newElement._formNode = this._formNode;
newElement._selectNode = this._selectNode;
newElement._textAreaNode = this._textAreaNode;
newElement._observers = this._observers;
newElement._isValue = this._isValue;
newElement._attributes = this._attributes;

(<INodeList<INode>>this.childNodes) = new NodeList();
(<IHTMLCollection<IElement>>this.children) = new HTMLCollection();
this._rootNode = null;
this._formNode = null;
this._selectNode = null;
this._textAreaNode = null;
this._observers = [];
this._isValue = null;
this._attributes = {};

for (let i = 0, max = this.parentNode.childNodes.length; i < max; i++) {
if (this.parentNode.childNodes[i] === this) {
this.parentNode.childNodes[i] = newElement;
break;
}
}

if ((<IElement>this.parentNode).children) {
for (let i = 0, max = (<IElement>this.parentNode).children.length; i < max; i++) {
if ((<IElement>this.parentNode).children[i] === this) {
(<IElement>this.parentNode).children[i] = newElement;
break;
}
}
}

if (newElement.isConnected && newElement.connectedCallback) {
newElement.connectedCallback();
}
}
};
callbacks[tagName] = callbacks[tagName] || [];
Expand Down
9 changes: 6 additions & 3 deletions packages/happy-dom/src/nodes/node/Node.ts
Expand Up @@ -406,12 +406,15 @@ export default class Node extends EventTarget implements INode {
if (this.isConnected !== isConnected) {
(<boolean>this.isConnected) = isConnected;

if (isConnected && this.connectedCallback) {
this.connectedCallback();
} else if (!isConnected && this.disconnectedCallback) {
if (!isConnected) {
if (this.ownerDocument['_activeElement'] === this) {
this.ownerDocument['_activeElement'] = null;
}
}

if (isConnected && this.connectedCallback) {
this.connectedCallback();
} else if (!isConnected && this.disconnectedCallback) {
this.disconnectedCallback();
}

Expand Down
10 changes: 8 additions & 2 deletions packages/happy-dom/test/CustomElement.ts
Expand Up @@ -48,16 +48,22 @@ export default class CustomElement extends new Window().HTMLElement {
span {
color: pink;
}
.class1 {
.propKey {
color: yellow;
}
</style>
<div>
<span class="class1">
<span class="propKey">
key1 is "${this.getAttribute('key1')}" and key2 is "${this.getAttribute(
'key2'
)}".
</span>
<span class="children">${this.childNodes
.map(
(child) =>
'#' + child['nodeType'] + (child['tagName'] || '') + child.textContent
)
.join(', ')}</span>
<span><slot></slot></span>
</div>
`;
Expand Down
Expand Up @@ -32,12 +32,53 @@ describe('HTMLUnknownElement', () => {

expect(parent.children.length).toBe(1);

expect(parent.children[0] !== element).toBe(true);
expect(parent.children[0] instanceof CustomElement).toBe(true);
expect(parent.children[0].shadowRoot.children.length).toBe(0);

document.body.appendChild(parent);

expect(parent.children[0].shadowRoot.children.length).toBe(2);
});

it('Copies all properties from the unknown element to the new instance.', () => {
const element = <HTMLUnknownElement>document.createElement('custom-element');
const child1 = document.createElement('div');
const child2 = document.createElement('div');

element.appendChild(child1);
element.appendChild(child2);

document.body.appendChild(element);

const childNodes = element.childNodes;
const children = element.children;
const rootNode = (element._rootNode = document.createElement('div'));
const formNode = (element._formNode = document.createElement('div'));
const selectNode = (element._selectNode = document.createElement('div'));
const textAreaNode = (element._textAreaNode = document.createElement('div'));
const observers = element._observers;
const isValue = (element._isValue = 'test');
const attributes = element._attributes;

window.customElements.define('custom-element', CustomElement);

const customElement = <CustomElement>document.body.children[0];

expect(document.body.children.length).toBe(1);
expect(customElement instanceof CustomElement).toBe(true);

expect(customElement.isConnected).toBe(true);
expect(customElement.shadowRoot.children.length).toBe(2);

expect(customElement.childNodes === childNodes).toBe(true);
expect(customElement.children === children).toBe(true);
expect(customElement._rootNode === rootNode).toBe(true);
expect(customElement._formNode === formNode).toBe(true);
expect(customElement._selectNode === selectNode).toBe(true);
expect(customElement._textAreaNode === textAreaNode).toBe(true);
expect(customElement._observers === observers).toBe(true);
expect(customElement._isValue === isValue).toBe(true);
expect(customElement._attributes === attributes).toBe(true);
});
});
});
68 changes: 66 additions & 2 deletions packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts
Expand Up @@ -166,14 +166,17 @@ describe('XMLSerializer', () => {
span {
color: pink;
}
.class1 {
.propKey {
color: yellow;
}
</style>
<div>
<span class="class1">
<span class="propKey">
key1 is "value1" and key2 is "value2".
</span>
<span class="children">
#1SPANSlottedcontent
</span>
<span><slot></slot></span>
</div>
</template>
Expand All @@ -183,6 +186,67 @@ describe('XMLSerializer', () => {
);
});

it('Renders the code from the documentation for server-side rendering as expected.', () => {
document.write(`
<html>
<head>
<title>Test page</title>
</head>
<body>
<div>
<my-custom-element>
<span>Slotted content</span>
</my-custom-element>
</div>
<script>
class MyCustomElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = \`
<style>
:host {
display: inline-block;
background: red;
}
</style>
<div><slot></slot></div>
\`;
}
}
customElements.define('my-custom-element', MyCustomElement);
</script>
</body>
</html>
`);

expect(
document.body
.querySelector('div')
.getInnerHTML({ includeShadowRoots: true })
.replace(/\s/gm, '')
).toBe(
`
<my-custom-element>
<span>Slotted content</span>
<template shadowrootmode="open">
<style>
:host {
display: inline-block;
background: red;
}
</style>
<div><slot></slot></div>
</template>
</my-custom-element>
`.replace(/\s/gm, '')
);
});

it('Does not escape unicode attributes.', () => {
const div = document.createElement('div');

Expand Down

0 comments on commit 556ee53

Please sign in to comment.