Skip to content

Commit 56a251c

Browse files
committed
Add support for rendering comment nodes
1 parent 87e5083 commit 56a251c

File tree

6 files changed

+168
-5
lines changed

6 files changed

+168
-5
lines changed

src/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export const COMMENT_TYPE = 8;
12
export const EMPTY_OBJ = {};
23
export const EMPTY_ARR = [];
34
export const IS_NON_DIMENSIONAL =

src/create-element.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { slice } from './util';
22
import options from './options';
3+
import { COMMENT_TYPE } from './constants';
34

45
let vnodeId = 0;
56

@@ -12,6 +13,10 @@ let vnodeId = 0;
1213
* @returns {import('./internal').VNode}
1314
*/
1415
export function createElement(type, props, children) {
16+
if (type === '!--') {
17+
return createVNode(COMMENT_TYPE, children, null, null);
18+
}
19+
1520
let normalizedProps = {},
1621
key,
1722
ref,

src/diff/index.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EMPTY_OBJ } from '../constants';
1+
import { COMMENT_TYPE, EMPTY_OBJ } from '../constants';
22
import { Component, getDomSibling } from '../component';
33
import { Fragment } from '../create-element';
44
import { diffChildren } from './children';
@@ -355,8 +355,11 @@ function diffElementNodes(
355355
// excessDomChildren so it isn't later removed in diffChildren
356356
if (
357357
child &&
358-
'setAttribute' in child === !!nodeType &&
359-
(nodeType ? child.localName === nodeType : child.nodeType === 3)
358+
(nodeType === COMMENT_TYPE
359+
? child.nodeType === COMMENT_TYPE
360+
: nodeType
361+
? child.localName === nodeType
362+
: child.nodeType === 3)
360363
) {
361364
dom = child;
362365
excessDomChildren[i] = null;
@@ -369,6 +372,9 @@ function diffElementNodes(
369372
if (nodeType === null) {
370373
// @ts-ignore createTextNode returns Text, we expect PreactElement
371374
return document.createTextNode(newProps);
375+
} else if (nodeType === COMMENT_TYPE) {
376+
// @ts-ignore createComment returns Comment, we expect PreactElement
377+
return document.createComment(newProps);
372378
}
373379

374380
if (isSvg) {
@@ -391,7 +397,7 @@ function diffElementNodes(
391397
isHydrating = false;
392398
}
393399

394-
if (nodeType === null) {
400+
if (nodeType === null || nodeType === COMMENT_TYPE) {
395401
// During hydration, we still have to split merged text from SSR'd HTML.
396402
if (oldProps !== newProps && (!isHydrating || dom.data !== newProps)) {
397403
dom.data = newProps;

src/index.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ export abstract class Component<P, S> {
187187
// Preact createElement
188188
// -----------------------------------
189189

190+
export function createElement(
191+
type: '!--',
192+
props: unknown,
193+
children: string
194+
): VNode<any>;
190195
export function createElement(
191196
type: 'input',
192197
props:
@@ -229,6 +234,7 @@ export namespace createElement {
229234
export import JSX = JSXInternal;
230235
}
231236

237+
export function h(type: '!--', props: unknown, children: string): VNode<any>;
232238
export function h(
233239
type: 'input',
234240
props:

src/internal.d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,11 @@ type RefObject<T> = { current: T | null };
102102
type RefCallback<T> = { (instance: T | null): void; current: undefined };
103103
type Ref<T> = RefObject<T> | RefCallback<T>;
104104

105+
type COMMENT_TYPE = 8;
106+
105107
export interface VNode<P = {}> extends preact.VNode<P> {
106108
// Redefine type here using our internal ComponentType type
107-
type: string | ComponentType<P>;
109+
type: string | ComponentType<P> | COMMENT_TYPE;
108110
props: P & { children: ComponentChildren };
109111
ref?: Ref<any> | null;
110112
_children: Array<VNode<any>> | null;

test/browser/comments.test.js

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { h, createElement, render, hydrate, Fragment } from 'preact';
2+
import { setupScratch, teardown } from '../_util/helpers';
3+
4+
/** @jsx createElement */
5+
/** @jsxFrag Fragment */
6+
7+
const COMMENT = '!--';
8+
9+
describe.only('keys', () => {
10+
/** @type {HTMLDivElement} */
11+
let scratch;
12+
13+
beforeEach(() => {
14+
scratch = setupScratch();
15+
});
16+
17+
afterEach(() => {
18+
teardown(scratch);
19+
});
20+
21+
it('should not render comments', () => {
22+
render(h(COMMENT, null, 'test'), scratch);
23+
expect(scratch.innerHTML).to.equal('<!--test-->');
24+
});
25+
26+
it('should render comments in elements', () => {
27+
render(<div>{h(COMMENT, null, 'test')}</div>, scratch);
28+
expect(scratch.innerHTML).to.equal('<div><!--test--></div>');
29+
});
30+
31+
it('should render Components that return comments', () => {
32+
function App() {
33+
return h(COMMENT, null, 'test');
34+
}
35+
render(<App />, scratch);
36+
expect(scratch.innerHTML).to.equal('<!--test-->');
37+
});
38+
39+
it('should render Fragments that wrap comments', () => {
40+
function App() {
41+
return <Fragment>{h(COMMENT, null, 'test')}</Fragment>;
42+
}
43+
render(<App />, scratch);
44+
expect(scratch.innerHTML).to.equal('<!--test-->');
45+
});
46+
47+
it('should render components that use comments to delimit start and end of a component', () => {
48+
function App() {
49+
return (
50+
<div>
51+
{h(COMMENT, null, 'start')}
52+
<div>test</div>
53+
{h(COMMENT, null, 'end')}
54+
</div>
55+
);
56+
}
57+
render(<App />, scratch);
58+
expect(scratch.innerHTML).to.equal(
59+
'<div><!--start--><div>test</div><!--end--></div>'
60+
);
61+
});
62+
63+
it('should render components that use comments to delimit start and end of a component with a Fragment', () => {
64+
function App() {
65+
return (
66+
<Fragment>
67+
{h(COMMENT, null, 'start')}
68+
<div>test</div>
69+
{h(COMMENT, null, 'end')}
70+
</Fragment>
71+
);
72+
}
73+
render(<App />, scratch);
74+
expect(scratch.innerHTML).to.equal('<!--start--><div>test</div><!--end-->');
75+
});
76+
77+
it('should move comments to the correct location when moving a component', () => {
78+
function Child() {
79+
return (
80+
<>
81+
{h(COMMENT, null, 'start')}
82+
<div>test</div>
83+
{h(COMMENT, null, 'end')}
84+
</>
85+
);
86+
}
87+
88+
/** @type {(props: { move?: boolean }) => any} */
89+
function App({ move = false }) {
90+
if (move) {
91+
return [
92+
<div key="a">a</div>,
93+
<Child key="child" />,
94+
<div key="b">b</div>
95+
];
96+
}
97+
98+
return [
99+
<Child key="child" />,
100+
<div key="a">a</div>,
101+
<div key="b">b</div>
102+
];
103+
}
104+
105+
const childHTML = '<!--start--><div>test</div><!--end-->';
106+
107+
render(<App />, scratch);
108+
expect(scratch.innerHTML).to.equal(`${childHTML}<div>a</div><div>b</div>`);
109+
110+
render(<App move />, scratch);
111+
expect(scratch.innerHTML).to.equal(`<div>a</div>${childHTML}<div>b</div>`);
112+
113+
render(<App />, scratch);
114+
expect(scratch.innerHTML).to.equal(`${childHTML}<div>a</div><div>b</div>`);
115+
});
116+
117+
it('should correctly show hide DOM around comments', () => {
118+
function App({ show = false }) {
119+
return (
120+
<>
121+
{h(COMMENT, null, 'start')}
122+
{show && <div>test</div>}
123+
{h(COMMENT, null, 'end')}
124+
</>
125+
);
126+
}
127+
128+
render(<App />, scratch);
129+
expect(scratch.innerHTML).to.equal('<!--start--><!--end-->');
130+
131+
render(<App show />, scratch);
132+
expect(scratch.innerHTML).to.equal('<!--start--><div>test</div><!--end-->');
133+
134+
render(<App />, scratch);
135+
expect(scratch.innerHTML).to.equal('<!--start--><!--end-->');
136+
});
137+
138+
it('should hydrate comments VNodes', () => {
139+
scratch.innerHTML = '<div><!--test--></div>';
140+
hydrate(<div>{h(COMMENT, null, 'test')}</div>, scratch);
141+
expect(scratch.innerHTML).to.equal('<div><!--test--></div>');
142+
});
143+
});

0 commit comments

Comments
 (0)