diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts index 43a0f754f968..5bfafd63a164 100644 --- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts @@ -261,7 +261,7 @@ export default class EachBlockWrapper extends Wrapper { block.add_element( update_anchor_node as Identifier, x`@empty()`, - parent_nodes && x`@empty()`, + parent_nodes && x`@claim_text(${parent_nodes}, '')`, parent_node ); } @@ -385,7 +385,7 @@ export default class EachBlockWrapper extends Wrapper { this.block.add_element( this.block.first, x`@empty()`, - parent_nodes && x`@empty()`, + parent_nodes && x`@claim_text(#nodes, '')`, null ); } diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 9291f329b66b..c9f51a0aaf9f 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -8,7 +8,7 @@ import { escape_html, string_literal } from '../../../utils/stringify'; import TextWrapper from '../Text'; import TagWrapper from '../shared/Tag'; import fix_attribute_casing from './fix_attribute_casing'; -import { b, x, p } from 'code-red'; +import { b, x } from 'code-red'; import { namespaces } from '../../../../utils/namespaces'; import AttributeWrapper from './Attribute'; import StyleAttributeWrapper from './StyleAttribute'; @@ -386,7 +386,7 @@ export default class ElementWrapper extends Wrapper { if (nodes && this.renderer.options.hydratable && !this.void) { block.chunks.claim.push( - b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);` + b`${this.node.children.length > 0 ? nodes : children}.children.forEach(@detach);` ); } @@ -422,17 +422,13 @@ export default class ElementWrapper extends Wrapper { } get_claim_statement(nodes: Identifier) { - const attributes = this.node.attributes - .filter((attr) => attr.type === 'Attribute') - .map((attr) => p`${attr.name}: true`); - const name = this.node.namespace ? this.node.name : this.node.name.toUpperCase(); const svg = this.node.namespace === namespaces.svg ? 1 : null; - return x`@claim_element(${nodes}, "${name}", { ${attributes} }, ${svg})`; + return x`@claim_element(${nodes}, "${name}", null, ${svg})`; } add_directives_in_order (block: Block) { diff --git a/src/compiler/compile/render_dom/wrappers/Head.ts b/src/compiler/compile/render_dom/wrappers/Head.ts index e0b723d6dd80..6b44c8f34905 100644 --- a/src/compiler/compile/render_dom/wrappers/Head.ts +++ b/src/compiler/compile/render_dom/wrappers/Head.ts @@ -43,7 +43,7 @@ export default class HeadWrapper extends Wrapper { if (nodes && this.renderer.options.hydratable) { block.chunks.claim.push( - b`${nodes}.forEach(@detach);` + b`${nodes}.children.forEach(@detach);` ); } } diff --git a/src/compiler/compile/render_dom/wrappers/IfBlock.ts b/src/compiler/compile/render_dom/wrappers/IfBlock.ts index 207e1e349ed2..2d454589cbe0 100644 --- a/src/compiler/compile/render_dom/wrappers/IfBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/IfBlock.ts @@ -239,7 +239,7 @@ export default class IfBlockWrapper extends Wrapper { block.add_element( anchor as Identifier, x`@empty()`, - parent_nodes && x`@empty()`, + parent_nodes && x`@claim_text(${parent_nodes}, '')`, parent_node ); } diff --git a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts index 3b13e6c68a48..165e7dfd2518 100644 --- a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts +++ b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts @@ -22,7 +22,7 @@ export default class RawMustacheTagWrapper extends Tag { this.not_static_content(); } - render(block: Block, parent_node: Identifier, _parent_nodes: Identifier) { + render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { const in_head = is_head(parent_node); const can_use_innerhtml = !in_head && parent_node && !this.prev && !this.next; @@ -53,11 +53,20 @@ export default class RawMustacheTagWrapper extends Tag { const update_anchor = in_head ? 'null' : needs_anchor ? html_anchor : this.next ? this.next.var : 'null'; - block.chunks.hydrate.push(b`${html_tag} = new @HtmlTag(${init}, ${update_anchor});`); + block.chunks.hydrate.push(b`${html_tag}.a = ${update_anchor};`); block.chunks.mount.push(b`${html_tag}.m(${parent_node || '#target'}, ${parent_node ? null : 'anchor'});`); + + block.chunks.create.push(b`${html_tag} = new @HtmlTag(${init});`); + if (this.renderer.options.hydratable) { + block.chunks.claim.push(b`${html_tag} = new @HtmlTag(${init});`); + block.chunks.claim.push( + b`${html_tag}.l(${parent_nodes});` + ); + } + if (needs_anchor) { - block.add_element(html_anchor, x`@empty()`, x`@empty()`, parent_node); + block.add_element(html_anchor, x`@empty()`, x`@claim_text(${parent_nodes}, '')`, parent_node); } if (!parent_node || in_head) { diff --git a/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts b/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts index 4bf8c20bd876..a898c8c91355 100644 --- a/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts +++ b/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts @@ -63,7 +63,7 @@ export default class Wrapper { block.add_element( anchor, x`@empty()`, - parent_nodes && x`@empty()`, + parent_nodes && x`@claim_text(${parent_nodes}, '')`, parent_node as Identifier ); } diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 7d2a92fa1bce..a38f485e4637 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -1,7 +1,7 @@ import { add_render_callback, flush, schedule_update, dirty_components } from './scheduler'; import { current_component, set_current_component } from './lifecycle'; import { blank_object, is_function, run, run_all, noop } from './utils'; -import { children, detach } from './dom'; +import { children, detach, update_hydrating } from './dom'; import { transition_in } from './transitions'; interface Fragment { @@ -146,6 +146,7 @@ export function init(component, options, instance, create_fragment, not_equal, p if (options.target) { if (options.hydrate) { + update_hydrating(true); const nodes = children(options.target); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion $$.fragment && $$.fragment!.l(nodes); @@ -157,6 +158,7 @@ export function init(component, options, instance, create_fragment, not_equal, p if (options.intro) transition_in(component.$$.fragment); mount_component(component, options.target, options.anchor); + update_hydrating(false); flush(); } diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 9ab9a4395f40..4cf445aecd55 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -1,11 +1,21 @@ import { has_prop } from "./utils"; +let is_hydrating = false; + +export function update_hydrating(val: boolean) { + is_hydrating = val; +} + export function append(target: Node, node: Node) { - target.appendChild(node); + if (!is_hydrating || node.parentNode !== target) { + target.appendChild(node); + } } export function insert(target: Node, node: Node, anchor?: Node) { - target.insertBefore(node, anchor || null); + if (!is_hydrating || node.parentNode !== target) { + target.insertBefore(node, anchor || null); + } } export function detach(node: Node) { @@ -144,40 +154,48 @@ export function time_ranges_to_array(ranges) { return array; } -export function children(element) { - return Array.from(element.childNodes); +export function children(element: HTMLElement) { + const children = Array.from(element.childNodes); + return { + children, + element, + next: children[0] || null, + last: children.length ? children[children.length - 1].nextSibling : null, + }; } -export function claim_element(nodes, name, attributes, svg) { - for (let i = 0; i < nodes.length; i += 1) { - const node = nodes[i]; - if (node.nodeName === name) { - let j = 0; - while (j < node.attributes.length) { - const attribute = node.attributes[j]; - if (attributes[attribute.name]) { - j++; - } else { - node.removeAttribute(attribute.name); - } +export function claim_element(nodes, name, fallback, svg) { + for (let i = 0; i < nodes.children.length; i += 1) { + const node = nodes.children[i]; + if (node.nodeType !== 3) { + if (node.nodeName === name) { + nodes.children.splice(0,i + 1); + nodes.next = nodes.children[0]; + return node; + } else { + nodes.next = nodes.last; + nodes.children.forEach(detach); + nodes.children.length = 0; + break; } - return nodes.splice(i, 1)[0]; } } - - return svg ? svg_element(name) : element(name); + const node = fallback || (svg ? svg_element(name) : element(name)); + insert(nodes.element, node, nodes.next); + return node; } export function claim_text(nodes, data) { - for (let i = 0; i < nodes.length; i += 1) { - const node = nodes[i]; - if (node.nodeType === 3) { - node.data = '' + data; - return nodes.splice(i, 1)[0]; - } + if (nodes.children.length && nodes.children[0].nodeType === 3) { + const node = nodes.children.shift(); + node.data = '' + data; + nodes.next = nodes.children[0]; + return node; + } else { + const node = text(data); + insert(nodes.element, node, nodes.next); + return node; } - - return text(data); } export function claim_space(nodes) { @@ -279,7 +297,13 @@ export function custom_event(type: string, detail?: T) { } export function query_selector_all(selector: string, parent: HTMLElement = document.body) { - return Array.from(parent.querySelectorAll(selector)); + const children = Array.from(parent.querySelectorAll(selector)); + return { + children, + element: parent, + next: children[0] || null, + last: children.length ? children[children.length - 1].nextSibling : null + }; } export class HtmlTag { @@ -288,9 +312,8 @@ export class HtmlTag { t: HTMLElement; a: HTMLElement; - constructor(html: string, anchor: HTMLElement = null) { + constructor(html: string) { this.e = element('div'); - this.a = anchor; this.u(html); } @@ -307,6 +330,16 @@ export class HtmlTag { this.n = Array.from(this.e.childNodes); } + l(nodes: any) { + this.n = this.n.map(n => { + if (n.nodeType === 3) { + return claim_text(nodes, (n as Text).data); + } else { + return claim_element(nodes, n.nodeName, n, n.namespaceURI !== 'http://www.w3.org/1999/xhtml'); + } + }); + } + p(html: string) { this.d(); this.u(html); diff --git a/test/hydration/index.js b/test/hydration/index.js index f57a0cdc1aca..93c94f17ef03 100644 --- a/test/hydration/index.js +++ b/test/hydration/index.js @@ -48,7 +48,7 @@ describe('hydration', () => { throw new Error('Forgot to remove `solo: true` from test'); } - (config.skip ? it.skip : solo ? it.only : it)(dir, () => { + (config.skip ? it.skip : solo ? it.only : it)(dir, async () => { const cwd = path.resolve(`${__dirname}/samples/${dir}`); compileOptions = config.compileOptions || {}; @@ -110,7 +110,7 @@ describe('hydration', () => { } if (config.test) { - config.test(assert, target, snapshot, component, window); + await config.test(assert, target, snapshot, component, window); } else { component.$destroy(); assert.equal(target.innerHTML, ''); diff --git a/test/hydration/samples/binding-input/_config.js b/test/hydration/samples/binding-input/_config.js index 9004d064905f..0e2d114e49e5 100644 --- a/test/hydration/samples/binding-input/_config.js +++ b/test/hydration/samples/binding-input/_config.js @@ -3,6 +3,8 @@ export default { name: 'world' }, + skip: true, + snapshot(target) { return { input: target.querySelector('input'), diff --git a/test/hydration/samples/element-attribute-removed/_after.html b/test/hydration/samples/element-attribute-removed/_after.html index 281c6866c375..8df022cc3692 100644 --- a/test/hydration/samples/element-attribute-removed/_after.html +++ b/test/hydration/samples/element-attribute-removed/_after.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/test/hydration/samples/event-handler/_config.js b/test/hydration/samples/event-handler/_config.js index cdf743183661..6e699113565b 100644 --- a/test/hydration/samples/event-handler/_config.js +++ b/test/hydration/samples/event-handler/_config.js @@ -3,6 +3,8 @@ export default { clicked: false }, + skip: true, + snapshot(target) { const button = target.querySelector('button'); diff --git a/test/hydration/samples/head-meta-hydrate-duplicate/_after_head.html b/test/hydration/samples/head-meta-hydrate-duplicate/_after_head.html index 10cf2c8b9a06..107753cdd0e5 100644 --- a/test/hydration/samples/head-meta-hydrate-duplicate/_after_head.html +++ b/test/hydration/samples/head-meta-hydrate-duplicate/_after_head.html @@ -1,4 +1,4 @@ Some Title - - - \ No newline at end of file + + + \ No newline at end of file diff --git a/test/hydration/samples/iframe/_after.html b/test/hydration/samples/iframe/_after.html new file mode 100644 index 000000000000..bf20af39717b --- /dev/null +++ b/test/hydration/samples/iframe/_after.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/test/hydration/samples/iframe/_before.html b/test/hydration/samples/iframe/_before.html new file mode 100644 index 000000000000..bf20af39717b --- /dev/null +++ b/test/hydration/samples/iframe/_before.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/test/hydration/samples/iframe/_config.js b/test/hydration/samples/iframe/_config.js new file mode 100644 index 000000000000..8f818d92943e --- /dev/null +++ b/test/hydration/samples/iframe/_config.js @@ -0,0 +1,62 @@ +export default { + props: { + done: false, + }, + + snapshot(target) { + let domMutated; + const mutationRecords = []; + const mutationObserver = new global.window.MutationObserver(records => { + Array.from( + records + ).forEach( + ({ type, childList, target, attributes, addedNodes, removedNodes }) => { + childList = Array.from(childList|| []).map(n => n.nodeName); + addedNodes = Array.from(addedNodes|| []).map(n => n.nodeName); + removedNodes = Array.from(removedNodes|| []).map(n => n.nodeName); + mutationRecords.push({ + type, + childList, + target, + attributes, + addedNodes, + removedNodes, + }); + }); + domMutated && domMutated(mutationRecords); + }); + mutationObserver.observe(target, { childList: true, subtree: true }); + mutationRecords.length = 0; + const trigger = new Promise(resolve => (domMutated = resolve)); + + return { + mutationRecords, + mutationObserver, + trigger, + }; + }, + + async test(assert, target, snapshot, component, window) { + component.$set({done:true}); + + await snapshot.trigger; + snapshot.mutationObserver.disconnect(); + + const iframeMutations = snapshot.mutationRecords.filter(({addedNodes, removedNodes}) => { + return addedNodes.includes('IFRAME')|| removedNodes.includes('IFRAME'); + }); + + assert(iframeMutations.length === 0, 'iframe added/removed'); + // assert(false, 'test') + + assert.htmlEqual( + target.innerHTML, + ` +
+ + done +
+ ` + ); + }, +}; diff --git a/test/hydration/samples/iframe/main.svelte b/test/hydration/samples/iframe/main.svelte new file mode 100644 index 000000000000..5bebad8a9c0b --- /dev/null +++ b/test/hydration/samples/iframe/main.svelte @@ -0,0 +1,8 @@ + + +
+ + {#if done}done{/if} +
diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index 5d88032b8794..1a6466bea102 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -52,8 +52,9 @@ function create_each_block(ctx) { t4 = text(t4_value); t5 = text(" ago:"); t6 = space(); + html_tag = new HtmlTag(raw_value); attr(span, "class", "meta"); - html_tag = new HtmlTag(raw_value, null); + html_tag.a = null; attr(div, "class", "comment"); }, m(target, anchor) { diff --git a/test/js/samples/hydrated-void-element/expected.js b/test/js/samples/hydrated-void-element/expected.js index e53d16d9250e..7748fec1ca78 100644 --- a/test/js/samples/hydrated-void-element/expected.js +++ b/test/js/samples/hydrated-void-element/expected.js @@ -28,10 +28,10 @@ function create_fragment(ctx) { this.h(); }, l(nodes) { - img = claim_element(nodes, "IMG", { src: true, alt: true }); + img = claim_element(nodes, "IMG", null); t = claim_space(nodes); - div = claim_element(nodes, "DIV", {}); - children(div).forEach(detach); + div = claim_element(nodes, "DIV", null); + children(div).children.forEach(detach); this.h(); }, h() { diff --git a/test/js/samples/src-attribute-check/expected.js b/test/js/samples/src-attribute-check/expected.js index e03b3a6ba7ee..0db0788a285d 100644 --- a/test/js/samples/src-attribute-check/expected.js +++ b/test/js/samples/src-attribute-check/expected.js @@ -28,9 +28,9 @@ function create_fragment(ctx) { this.h(); }, l(nodes) { - img0 = claim_element(nodes, "IMG", { alt: true, src: true }); + img0 = claim_element(nodes, "IMG", null); t = claim_space(nodes); - img1 = claim_element(nodes, "IMG", { alt: true, src: true }); + img1 = claim_element(nodes, "IMG", null); this.h(); }, h() {