diff --git a/src/constants.js b/src/constants.js
index c30ffc381f..d68daf65b1 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -1,3 +1,4 @@
+export const COMMENT_TYPE = 8;
export const EMPTY_OBJ = {};
export const EMPTY_ARR = [];
export const IS_NON_DIMENSIONAL =
diff --git a/src/create-element.js b/src/create-element.js
index 892389054a..c9d4b4809f 100644
--- a/src/create-element.js
+++ b/src/create-element.js
@@ -1,5 +1,6 @@
import { slice } from './util';
import options from './options';
+import { COMMENT_TYPE } from './constants';
let vnodeId = 0;
@@ -12,6 +13,10 @@ let vnodeId = 0;
* @returns {import('./internal').VNode}
*/
export function createElement(type, props, children) {
+ if (type === '!--') {
+ return createVNode(COMMENT_TYPE, children, null, null);
+ }
+
let normalizedProps = {},
key,
ref,
diff --git a/src/diff/index.js b/src/diff/index.js
index b6243a8bef..ee9b61767d 100644
--- a/src/diff/index.js
+++ b/src/diff/index.js
@@ -1,4 +1,4 @@
-import { EMPTY_OBJ } from '../constants';
+import { COMMENT_TYPE, EMPTY_OBJ } from '../constants';
import { Component, getDomSibling } from '../component';
import { Fragment } from '../create-element';
import { diffChildren } from './children';
@@ -355,8 +355,11 @@ function diffElementNodes(
// excessDomChildren so it isn't later removed in diffChildren
if (
child &&
- 'setAttribute' in child === !!nodeType &&
- (nodeType ? child.localName === nodeType : child.nodeType === 3)
+ (nodeType === COMMENT_TYPE
+ ? child.nodeType === COMMENT_TYPE
+ : nodeType
+ ? child.localName === nodeType
+ : child.nodeType === 3)
) {
dom = child;
excessDomChildren[i] = null;
@@ -369,6 +372,9 @@ function diffElementNodes(
if (nodeType === null) {
// @ts-ignore createTextNode returns Text, we expect PreactElement
return document.createTextNode(newProps);
+ } else if (nodeType === COMMENT_TYPE) {
+ // @ts-ignore createComment returns Comment, we expect PreactElement
+ return document.createComment(newProps);
}
if (isSvg) {
@@ -391,7 +397,7 @@ function diffElementNodes(
isHydrating = false;
}
- if (nodeType === null) {
+ if (nodeType === null || nodeType === COMMENT_TYPE) {
// During hydration, we still have to split merged text from SSR'd HTML.
if (oldProps !== newProps && (!isHydrating || dom.data !== newProps)) {
dom.data = newProps;
diff --git a/src/index.d.ts b/src/index.d.ts
index eb192aa15c..1e8637b241 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -187,6 +187,11 @@ export abstract class Component
{
// Preact createElement
// -----------------------------------
+export function createElement(
+ type: '!--',
+ props: unknown,
+ children: string
+): VNode;
export function createElement(
type: 'input',
props:
@@ -229,6 +234,7 @@ export namespace createElement {
export import JSX = JSXInternal;
}
+export function h(type: '!--', props: unknown, children: string): VNode;
export function h(
type: 'input',
props:
diff --git a/src/internal.d.ts b/src/internal.d.ts
index 57faa0ded0..224cac7f79 100644
--- a/src/internal.d.ts
+++ b/src/internal.d.ts
@@ -102,9 +102,11 @@ type RefObject = { current: T | null };
type RefCallback = { (instance: T | null): void; current: undefined };
type Ref = RefObject | RefCallback;
+type COMMENT_TYPE = 8;
+
export interface VNode extends preact.VNode
{
// Redefine type here using our internal ComponentType type
- type: string | ComponentType
;
+ type: string | ComponentType
| COMMENT_TYPE;
props: P & { children: ComponentChildren };
ref?: Ref | null;
_children: Array> | null;
diff --git a/test/browser/comments.test.js b/test/browser/comments.test.js
new file mode 100644
index 0000000000..1ac65637c6
--- /dev/null
+++ b/test/browser/comments.test.js
@@ -0,0 +1,143 @@
+import { h, createElement, render, hydrate, Fragment } from 'preact';
+import { setupScratch, teardown } from '../_util/helpers';
+
+/** @jsx createElement */
+/** @jsxFrag Fragment */
+
+const COMMENT = '!--';
+
+describe('keys', () => {
+ /** @type {HTMLDivElement} */
+ let scratch;
+
+ beforeEach(() => {
+ scratch = setupScratch();
+ });
+
+ afterEach(() => {
+ teardown(scratch);
+ });
+
+ it('should not render comments', () => {
+ render(h(COMMENT, null, 'test'), scratch);
+ expect(scratch.innerHTML).to.equal('');
+ });
+
+ it('should render comments in elements', () => {
+ render({h(COMMENT, null, 'test')}
, scratch);
+ expect(scratch.innerHTML).to.equal('');
+ });
+
+ it('should render Components that return comments', () => {
+ function App() {
+ return h(COMMENT, null, 'test');
+ }
+ render(, scratch);
+ expect(scratch.innerHTML).to.equal('');
+ });
+
+ it('should render Fragments that wrap comments', () => {
+ function App() {
+ return {h(COMMENT, null, 'test')};
+ }
+ render(, scratch);
+ expect(scratch.innerHTML).to.equal('');
+ });
+
+ it('should render components that use comments to delimit start and end of a component', () => {
+ function App() {
+ return (
+
+ {h(COMMENT, null, 'start')}
+
test
+ {h(COMMENT, null, 'end')}
+
+ );
+ }
+ render(, scratch);
+ expect(scratch.innerHTML).to.equal(
+ ''
+ );
+ });
+
+ it('should render components that use comments to delimit start and end of a component with a Fragment', () => {
+ function App() {
+ return (
+
+ {h(COMMENT, null, 'start')}
+ test
+ {h(COMMENT, null, 'end')}
+
+ );
+ }
+ render(, scratch);
+ expect(scratch.innerHTML).to.equal('test
');
+ });
+
+ it('should move comments to the correct location when moving a component', () => {
+ function Child() {
+ return (
+ <>
+ {h(COMMENT, null, 'start')}
+ test
+ {h(COMMENT, null, 'end')}
+ >
+ );
+ }
+
+ /** @type {(props: { move?: boolean }) => any} */
+ function App({ move = false }) {
+ if (move) {
+ return [
+ a
,
+ ,
+ b
+ ];
+ }
+
+ return [
+ ,
+ a
,
+ b
+ ];
+ }
+
+ const childHTML = 'test
';
+
+ render(, scratch);
+ expect(scratch.innerHTML).to.equal(`${childHTML}a
b
`);
+
+ render(, scratch);
+ expect(scratch.innerHTML).to.equal(`a
${childHTML}b
`);
+
+ render(, scratch);
+ expect(scratch.innerHTML).to.equal(`${childHTML}a
b
`);
+ });
+
+ it('should correctly show hide DOM around comments', () => {
+ function App({ show = false }) {
+ return (
+ <>
+ {h(COMMENT, null, 'start')}
+ {show && test
}
+ {h(COMMENT, null, 'end')}
+ >
+ );
+ }
+
+ render(, scratch);
+ expect(scratch.innerHTML).to.equal('');
+
+ render(, scratch);
+ expect(scratch.innerHTML).to.equal('test
');
+
+ render(, scratch);
+ expect(scratch.innerHTML).to.equal('');
+ });
+
+ it('should hydrate comments VNodes', () => {
+ scratch.innerHTML = '';
+ hydrate({h(COMMENT, null, 'test')}
, scratch);
+ expect(scratch.innerHTML).to.equal('');
+ });
+});