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( + '
test
' + ); + }); + + 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('
'); + }); +});