diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index f118d77761..46c20b7542 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -33,6 +33,8 @@ import { EACH_IS_STRICT_EQUALS, EACH_ITEM_REACTIVE, EACH_KEYED, + TEMPLATE_FRAGMENT, + TEMPLATE_USE_IMPORT_NODE, TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT @@ -929,9 +931,9 @@ function serialize_bind_this(bind_this, context, node) { * const block_name = $.template(`...`); * * // for the main block: - * const id = $.open(block_name); + * const id = block_name(); * // init stuff and possibly render effect - * $.close(id); + * $.close($$anchor, id); * ``` * Adds the hoisted parts to `context.state.hoisted` and returns the statements of the main block. * @param {import('#compiler').SvelteNode} parent @@ -1001,24 +1003,18 @@ function create_block(parent, name, nodes, context) { node: id }); - context.state.hoisted.push( - b.var( - template_name, - b.call( - get_template_function(namespace, state), - b.template([b.quasi(state.template.join(''), true)], []) - ) - ) - ); - /** @type {import('estree').Expression[]} */ - const args = [template_name]; + const args = [b.template([b.quasi(state.template.join(''), true)], [])]; if (state.metadata.context.template_needs_import_node) { - args.push(b.false); + args.push(b.literal(TEMPLATE_USE_IMPORT_NODE)); } - body.push(b.var(id, b.call('$.open', ...args)), ...state.before_init, ...state.init); + context.state.hoisted.push( + b.var(template_name, b.call(get_template_function(namespace, state), ...args)) + ); + + body.push(b.var(id, b.call(template_name)), ...state.before_init, ...state.init); close = b.stmt(b.call('$.close', b.id('$$anchor'), id)); } else if (is_single_child_not_needing_template) { context.visit(trimmed[0], state); @@ -1054,25 +1050,24 @@ function create_block(parent, name, nodes, context) { // special case — we can use `$.comment` instead of creating a unique template body.push(b.var(id, b.call('$.comment'))); } else { + let flags = TEMPLATE_FRAGMENT; + + if (state.metadata.context.template_needs_import_node) { + flags |= TEMPLATE_USE_IMPORT_NODE; + } + state.hoisted.push( b.var( template_name, b.call( get_template_function(namespace, state), b.template([b.quasi(state.template.join(''), true)], []), - b.true + b.literal(flags) ) ) ); - /** @type {import('estree').Expression[]} */ - const args = [template_name]; - - if (state.metadata.context.template_needs_import_node) { - args.push(b.false); - } - - body.push(b.var(id, b.call('$.open_frag', ...args))); + body.push(b.var(id, b.call(template_name))); } body.push(...state.before_init, ...state.init); diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index f26a567506..0015524532 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -16,6 +16,9 @@ export const TRANSITION_IN = 1; export const TRANSITION_OUT = 1 << 1; export const TRANSITION_GLOBAL = 1 << 2; +export const TEMPLATE_FRAGMENT = 1; +export const TEMPLATE_USE_IMPORT_NODE = 1 << 1; + /** List of Element events that will be delegated */ export const DelegatedEvents = [ 'beforeinput', diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js index 0f6cb7dc67..e1dda95d8b 100644 --- a/packages/svelte/src/internal/client/dom/elements/custom-element.js +++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js @@ -1,6 +1,6 @@ import { createClassComponent } from '../../../../legacy/legacy-client.js'; import { destroy_effect, render_effect } from '../../reactivity/effects.js'; -import { open, close } from '../template.js'; +import { close } from '../template.js'; import { define_property } from '../../utils.js'; /** @@ -98,14 +98,10 @@ if (typeof HTMLElement === 'function') { * @param {Element} anchor */ return (anchor) => { - const node = open(() => { - const slot = document.createElement('slot'); - if (name !== 'default') { - slot.name = name; - } - return slot; - }); - close(anchor, /** @type {Element} */ (node)); + const slot = document.createElement('slot'); + if (name !== 'default') slot.name = name; + + close(anchor, slot); }; } /** @type {Record} */ diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js index 780a7afe0c..42ee54c107 100644 --- a/packages/svelte/src/internal/client/dom/reconciler.js +++ b/packages/svelte/src/internal/client/dom/reconciler.js @@ -8,25 +8,6 @@ export function create_fragment_from_html(html) { return elem.content; } -/** - * Creating a document fragment from HTML that contains script tags will not execute - * the scripts. We need to replace the script tags with new ones so that they are executed. - * @param {string} html - */ -export function create_fragment_with_script_from_html(html) { - var content = create_fragment_from_html(html); - var scripts = content.querySelectorAll('script'); - for (const script of scripts) { - var new_script = document.createElement('script'); - for (var i = 0; i < script.attributes.length; i++) { - new_script.setAttribute(script.attributes[i].name, script.attributes[i].value); - } - new_script.textContent = script.textContent; - /** @type {Node} */ (script.parentNode).replaceChild(new_script, script); - } - return content; -} - /** * @param {import('#client').Dom} current * @param {Text | Element | Comment} sibling diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index cfe365f570..6588e1ecd5 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -1,121 +1,134 @@ import { hydrate_nodes, hydrating } from './hydration.js'; -import { child, clone_node, empty } from './operations.js'; -import { - create_fragment_from_html, - create_fragment_with_script_from_html, - insert -} from './reconciler.js'; +import { clone_node, empty } from './operations.js'; +import { create_fragment_from_html, insert } from './reconciler.js'; import { current_effect } from '../runtime.js'; -import { is_array } from '../utils.js'; +import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; /** * @param {string} html - * @param {boolean} return_fragment - * @returns {() => Node} + * @param {number} flags + * @returns {() => Node | Node[]} */ /*#__NO_SIDE_EFFECTS__*/ -export function template(html, return_fragment) { - /** @type {undefined | Node} */ - let cached_content; +export function template(html, flags) { + var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0; + var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0; + + /** @type {Node} */ + var node; + return () => { - if (cached_content === undefined) { - const content = create_fragment_from_html(html); - cached_content = return_fragment ? content : /** @type {Node} */ (child(content)); + if (hydrating) { + return is_fragment ? hydrate_nodes : /** @type {Node} */ (hydrate_nodes[0]); } - return cached_content; + + if (!node) { + node = create_fragment_from_html(html); + if (!is_fragment) node = /** @type {Node} */ (node.firstChild); + } + + return use_import_node ? document.importNode(node, true) : clone_node(node, true); }; } /** * @param {string} html - * @param {boolean} return_fragment - * @returns {() => Node} + * @param {number} flags + * @returns {() => Node | Node[]} */ /*#__NO_SIDE_EFFECTS__*/ -export function template_with_script(html, return_fragment) { - /** @type {undefined | Node} */ - let cached_content; +export function template_with_script(html, flags) { + var first = true; + var fn = template(html, flags); + return () => { - if (cached_content === undefined) { - const content = create_fragment_with_script_from_html(html); - cached_content = return_fragment ? content : /** @type {Node} */ (child(content)); + if (hydrating) return fn(); + + var node = /** @type {Element | DocumentFragment} */ (fn()); + + if (first) { + first = false; + run_scripts(node); } - return cached_content; + + return node; }; } /** * @param {string} svg - * @param {boolean} return_fragment - * @returns {() => Node} + * @param {number} flags + * @returns {() => Node | Node[]} */ /*#__NO_SIDE_EFFECTS__*/ -export function svg_template(svg, return_fragment) { - /** @type {undefined | Node} */ - let cached_content; +export function svg_template(svg, flags) { + var fn = template(`${svg}`, flags & ~TEMPLATE_FRAGMENT); + + /** @type {Element | DocumentFragment} */ + var node; + return () => { - if (cached_content === undefined) { - const content = /** @type {Node} */ (child(create_fragment_from_html(`${svg}`))); - cached_content = return_fragment ? content : /** @type {Node} */ (child(content)); + if (hydrating) { + return fn(); + } + + if (!node) { + var element = /** @type {Element} */ (fn()); + + if ((flags & TEMPLATE_FRAGMENT) === 0) { + node = /** @type {Element} */ (element.firstChild); + } else { + node = document.createDocumentFragment(); + while (element.firstChild) { + node.appendChild(element.firstChild); + } + } } - return cached_content; + + return node; }; } /** * @param {string} svg - * @param {boolean} return_fragment - * @returns {() => Node} + * @param {number} flags + * @returns {() => Node | Node[]} */ /*#__NO_SIDE_EFFECTS__*/ -export function svg_template_with_script(svg, return_fragment) { - /** @type {undefined | Node} */ - let cached_content; +export function svg_template_with_script(svg, flags) { + var first = true; + var fn = svg_template(svg, flags); + return () => { - if (cached_content === undefined) { - const content = /** @type {Node} */ (child(create_fragment_from_html(`${svg}`))); - cached_content = return_fragment ? content : /** @type {Node} */ (child(content)); - } - return cached_content; - }; -} + if (hydrating) return fn(); -/** - * @param {boolean} is_fragment - * @param {boolean} use_clone_node - * @param {() => Node} [template_element_fn] - * @returns {Element | DocumentFragment | Node[]} - */ -/*#__NO_SIDE_EFFECTS__*/ -function open_template(is_fragment, use_clone_node, template_element_fn) { - if (hydrating) { - return is_fragment ? hydrate_nodes : /** @type {Element} */ (hydrate_nodes[0]); - } + var node = /** @type {Element | DocumentFragment} */ (fn()); - return use_clone_node - ? clone_node(/** @type {() => Element} */ (template_element_fn)(), true) - : document.importNode(/** @type {() => Element} */ (template_element_fn)(), true); -} + if (first) { + first = false; + run_scripts(node); + } -/** - * @param {() => Node} template_element_fn - * @param {boolean} [use_clone_node] - * @returns {Element} - */ -export function open(template_element_fn, use_clone_node = true) { - return /** @type {Element} */ (open_template(false, use_clone_node, template_element_fn)); + return node; + }; } /** - * @param {() => Node} template_element_fn - * @param {boolean} [use_clone_node] - * @returns {Element | DocumentFragment | Node[]} + * Creating a document fragment from HTML that contains script tags will not execute + * the scripts. We need to replace the script tags with new ones so that they are executed. + * @param {Element | DocumentFragment} node */ -export function open_frag(template_element_fn, use_clone_node = true) { - return open_template(true, use_clone_node, template_element_fn); -} +function run_scripts(node) { + for (const script of node.querySelectorAll('script')) { + var clone = document.createElement('script'); + for (var attribute of script.attributes) { + clone.setAttribute(attribute.name, attribute.value); + } -const comment_template = template('', true); + clone.textContent = script.textContent; + script.replaceWith(clone); + } +} /** * @param {Text | Comment | Element} anchor @@ -136,9 +149,7 @@ export function text(anchor) { } /*#__NO_SIDE_EFFECTS__*/ -export function comment() { - return open_frag(comment_template); -} +export const comment = template('', TEMPLATE_FRAGMENT); /** * Assign the created (or in hydration mode, traversed) dom elements to the current block