Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 108 additions & 14 deletions src/diff/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,19 +255,93 @@ export function diff(
? cloneNode(tmp.props.children)
: tmp;

oldDom = diffChildren(
parentDom,
isArray(renderResult) ? renderResult : [renderResult],
newVNode,
oldVNode,
globalContext,
namespace,
excessDomChildren,
commitQueue,
oldDom,
isHydrating,
refQueue
);
// Teardown old html fragment markers if content changed or transitioning away
if (newType === Fragment && oldVNode._dom && oldVNode._dom._endHtml) {
const endMarker = oldVNode._dom._endHtml;
oldDom = endMarker.nextSibling;
const newDSIH = newVNode.props.dangerouslySetInnerHTML;
if (
!newDSIH ||
newDSIH.__html !== oldVNode.props.dangerouslySetInnerHTML.__html
) {
removeRange(oldVNode._dom, endMarker);
} else {
newVNode._dom = oldVNode._dom;
}
}

if (newType === Fragment && newVNode.props.dangerouslySetInnerHTML) {
const html = newVNode.props.dangerouslySetInnerHTML.__html;
newVNode._children = [];

// Unmount old children if transitioning from normal children
if (oldVNode._children && oldVNode._children.length) {
oldDom = getDomSibling(oldVNode);
for (let j = 0; j < oldVNode._children.length; j++) {
if (oldVNode._children[j]) {
unmount(oldVNode._children[j], newVNode);
}
}
}

if (isHydrating) {
// Find start/end markers in excessDomChildren.
// We can't rely on oldDom because insert() skips comment nodes.
let startMarker, endMarker;
if (excessDomChildren) {
for (let j = 0; j < excessDomChildren.length; j++) {
const edc = excessDomChildren[j];
if (!edc) continue;
if (!startMarker && edc.nodeType === 8 && edc.data === '$h') {
startMarker = edc;
}
if (startMarker) {
excessDomChildren[j] = NULL;
if (edc.nodeType === 8 && edc.data === '/$h') {
endMarker = edc;
break;
}
}
}
}
if (startMarker) {
newVNode._dom = startMarker;
startMarker._endHtml = endMarker;
}
oldDom = endMarker ? endMarker.nextSibling : NULL;
}

// Mount: first render or update that cleared old markers
if (!newVNode._dom) {
const startMarker = document.createComment('$h');
const endMarker = document.createComment('/$h');
startMarker._endHtml = endMarker;

parentDom.insertBefore(startMarker, oldDom);
if (html) {
const tpl = document.createElement('template');
tpl.innerHTML = html;
parentDom.insertBefore(tpl.content, oldDom);
}
parentDom.insertBefore(endMarker, oldDom);

newVNode._dom = startMarker;
}
} else {
oldDom = diffChildren(
parentDom,
isArray(renderResult) ? renderResult : [renderResult],
newVNode,
oldVNode,
globalContext,
namespace,
excessDomChildren,
commitQueue,
oldDom,
isHydrating,
refQueue
);
}

c.base = newVNode._dom;

Expand Down Expand Up @@ -331,6 +405,11 @@ export function diff(

if ((tmp = options.diffed)) tmp(newVNode);

// For html Fragments (bail-out path), advance oldDom past the end marker
if (newType === Fragment && newVNode._dom && newVNode._dom._endHtml) {
return newVNode._dom._endHtml.nextSibling;
}

return newVNode._flags & MODE_SUSPENDED ? undefined : oldDom;
}

Expand All @@ -341,6 +420,17 @@ function markAsForce(vnode) {
}
}

/** Remove all DOM nodes from `start` through `end` inclusive. */
function removeRange(start, end) {
let node = start;
while (node) {
let next = node.nextSibling;
removeNode(node);
if (node === end) break;
node = next;
}
}

/**
* @param {Array<Component>} commitQueue List of components
* which have callbacks to invoke in commitRoot
Expand Down Expand Up @@ -661,7 +751,11 @@ export function unmount(vnode, parentVNode, skipRemove) {
}

if (!skipRemove) {
removeNode(vnode._dom);
if (vnode.type === Fragment && vnode._dom && vnode._dom._endHtml) {
removeRange(vnode._dom, vnode._dom._endHtml);
} else {
removeNode(vnode._dom);
}
}

vnode._component = vnode._parent = vnode._dom = UNDEFINED;
Expand Down
4 changes: 3 additions & 1 deletion src/index-5.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,9 @@ export function cloneElement<P>(
// -----------------------------------

// TODO: Revisit what the public type of this is...
export const Fragment: FunctionComponent<{}>;
export const Fragment: FunctionComponent<{
dangerouslySetInnerHTML?: PreactDOMAttributes['dangerouslySetInnerHTML'];
}>;

//
// Preact options
Expand Down
4 changes: 3 additions & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,9 @@ export function cloneElement<P>(
// -----------------------------------

// TODO: Revisit what the public type of this is...
export const Fragment: FunctionComponent<{}>;
export const Fragment: FunctionComponent<{
dangerouslySetInnerHTML?: PreactDOMAttributes['dangerouslySetInnerHTML'];
}>;

//
// Preact options
Expand Down
95 changes: 95 additions & 0 deletions test/browser/hydrate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -515,4 +515,99 @@ describe('hydrate()', () => {
rerender();
expect(scratch.innerHTML).to.equal('<div>Error!</div>');
});

describe('Fragment dangerouslySetInnerHTML', () => {
it('should hydrate Fragment with dangerouslySetInnerHTML', () => {
scratch.innerHTML =
'<div>before<!--$h--><b>content</b><!--/$h-->after</div>';
clearLog();

hydrate(
<div>
before
<Fragment dangerouslySetInnerHTML={{ __html: '<b>content</b>' }} />
after
</div>,
scratch
);

expect(serializeHtml(scratch)).to.equal(
'<div>before<!--$h--><b>content</b><!--/$h-->after</div>'
);
expect(getLog()).to.deep.equal([]);
});

it('should hydrate then update Fragment dangerouslySetInnerHTML', () => {
scratch.innerHTML = '<div><!--$h--><b>first</b><!--/$h--></div>';
clearLog();

/** @type {(v) => void} */
let set;
class App extends Component {
constructor(props) {
super(props);
set = this.setState.bind(this);
this.state = { html: '<b>first</b>' };
}
render() {
return (
<div>
<Fragment dangerouslySetInnerHTML={{ __html: this.state.html }} />
</div>
);
}
}

hydrate(<App />, scratch);
expect(serializeHtml(scratch)).to.equal(
'<div><!--$h--><b>first</b><!--/$h--></div>'
);
expect(getLog()).to.deep.equal([]);

set({ html: '<i>second</i>' });
rerender();
expect(serializeHtml(scratch)).to.equal(
'<div><!--$h--><i>second</i><!--/$h--></div>'
);
});

it('should hydrate with siblings around html Fragment', () => {
scratch.innerHTML =
'<div><span>before</span><!--$h--><b>html</b><!--/$h--><span>after</span></div>';
clearLog();

hydrate(
<div>
<span>before</span>
<Fragment dangerouslySetInnerHTML={{ __html: '<b>html</b>' }} />
<span>after</span>
</div>,
scratch
);

expect(serializeHtml(scratch)).to.equal(
'<div><span>before</span><!--$h--><b>html</b><!--/$h--><span>after</span></div>'
);
expect(getLog()).to.deep.equal([]);
});

it('should hydrate multiple html Fragments as siblings', () => {
scratch.innerHTML =
'<div><!--$h--><b>a</b><!--/$h--><!--$h--><i>b</i><!--/$h--></div>';
clearLog();

hydrate(
<div>
<Fragment dangerouslySetInnerHTML={{ __html: '<b>a</b>' }} />
<Fragment dangerouslySetInnerHTML={{ __html: '<i>b</i>' }} />
</div>,
scratch
);

expect(serializeHtml(scratch)).to.equal(
'<div><!--$h--><b>a</b><!--/$h--><!--$h--><i>b</i><!--/$h--></div>'
);
expect(getLog()).to.deep.equal([]);
});
});
});
Loading
Loading