chore: simplify templates (#10954)

* WIP

* WIP

* fix

* simplify

* rename close to append

* appease typescript

* simplify

* simplify

* frag -> root

* move logic to where it's used
pull/10956/head
Rich Harris 12 months ago committed by GitHub
parent faf838c46d
commit 326e2b4840
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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);
* $.append($$anchor, id);
* ```
* Adds the hoisted parts to `context.state.hoisted` and returns the statements of the main block.
* @param {import('#compiler').SvelteNode} parent
@ -1001,25 +1003,19 @@ 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);
close = b.stmt(b.call('$.close', b.id('$$anchor'), id));
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('$.append', b.id('$$anchor'), id));
} else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state);
body.push(...state.before_init, ...state.init);
@ -1040,7 +1036,7 @@ function create_block(parent, name, nodes, context) {
});
body.push(b.var(id, b.call('$.text', b.id('$$anchor'))), ...state.before_init, ...state.init);
close = b.stmt(b.call('$.close', b.id('$$anchor'), id));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else {
/** @type {(is_text: boolean) => import('estree').Expression} */
const expression = (is_text) =>
@ -1054,30 +1050,29 @@ 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);
close = b.stmt(b.call('$.close_frag', b.id('$$anchor'), id));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
}
} else {
body.push(...state.before_init, ...state.init);
@ -1093,12 +1088,6 @@ function create_block(parent, name, nodes, context) {
// It's important that close is the last statement in the block, as any previous statements
// could contain element insertions into the template, which the close statement needs to
// know of when constructing the list of current inner elements.
if (context.path.length > 0) {
// this is a block — return DOM so it can be attached directly to the effect
close = b.return(close.expression);
}
body.push(close);
}
@ -1566,7 +1555,7 @@ function serialize_template_literal(values, visit) {
/** @type {import('../types').ComponentVisitors} */
export const template_visitors = {
Fragment(node, context) {
const body = create_block(node, 'frag', node.nodes, context);
const body = create_block(node, 'root', node.nodes, context);
return b.block(body);
},
Comment(node, context) {

@ -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',

@ -8,7 +8,7 @@ import {
} from '../../../../constants.js';
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import { insert, remove } from '../reconciler.js';
import { remove } from '../reconciler.js';
import { untrack } from '../../runtime.js';
import {
block,
@ -389,7 +389,7 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
if (moved && index !== LIS_ITEM) {
if (last_item !== undefined) anchor = get_first_child(last_item);
insert(/** @type {import('#client').Dom} */ (item.e.dom), anchor);
move(/** @type {import('#client').Dom} */ (item.e.dom), anchor);
}
}
@ -560,3 +560,17 @@ function create_item(anchor, value, key, index, render_fn, flags) {
current_each_item = previous_each_item;
}
}
/**
* @param {import('#client').Dom} current
* @param {Text | Element | Comment} anchor
*/
function move(current, anchor) {
if (is_array(current)) {
for (var i = 0; i < current.length; i++) {
anchor.before(current[i]);
}
} else {
anchor.before(current);
}
}

@ -1,6 +1,4 @@
import { render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { untrack } from '../../runtime.js';
import { branch, render_effect } from '../../reactivity/effects.js';
/**
* @template {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} SnippetFn
@ -11,19 +9,13 @@ import { untrack } from '../../runtime.js';
*/
export function snippet(get_snippet, node, ...args) {
/** @type {SnippetFn | null | undefined} */
var snippet_fn;
var snippet;
render_effect(() => {
if (snippet_fn === (snippet_fn = get_snippet())) return;
if (snippet === (snippet = get_snippet())) return;
if (snippet_fn) {
// Untrack so we only rerender when the snippet function itself changes,
// not when an eagerly-read prop inside the snippet function changes
var dom = untrack(() => /** @type {SnippetFn} */ (snippet_fn)(node, ...args));
if (dom !== undefined) {
return () => remove(dom);
}
if (snippet) {
branch(() => /** @type {SnippetFn} */ (snippet)(node, ...args));
}
});
}

@ -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 { append } 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;
append(anchor, slot);
};
}
/** @type {Record<string, any>} */

@ -8,41 +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
*/
export function insert(current, sibling) {
if (!current) return sibling;
if (is_array(current)) {
for (var i = 0; i < current.length; i++) {
sibling.before(/** @type {Node} */ (current[i]));
}
} else {
sibling.before(/** @type {Node} */ (current));
}
}
/**
* @param {import('#client').Dom} current
*/

@ -1,121 +1,135 @@
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 } 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 {string} content
* @param {number} flags
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
export function template(html, return_fragment) {
/** @type {undefined | Node} */
let cached_content;
export function template(content, 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;
};
}
/**
* @param {string} html
* @param {boolean} return_fragment
* @returns {() => Node}
*/
/*#__NO_SIDE_EFFECTS__*/
export function template_with_script(html, return_fragment) {
/** @type {undefined | Node} */
let cached_content;
return () => {
if (cached_content === undefined) {
const content = create_fragment_with_script_from_html(html);
cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
if (!node) {
node = create_fragment_from_html(content);
if (!is_fragment) node = /** @type {Node} */ (node.firstChild);
}
return cached_content;
return use_import_node ? document.importNode(node, true) : clone_node(node, true);
};
}
/**
* @param {string} svg
* @param {boolean} return_fragment
* @returns {() => Node}
* @param {string} content
* @param {number} flags
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
export function svg_template(svg, return_fragment) {
/** @type {undefined | Node} */
let cached_content;
export function template_with_script(content, flags) {
var first = true;
var fn = template(content, flags);
return () => {
if (cached_content === undefined) {
const content = /** @type {Node} */ (child(create_fragment_from_html(`<svg>${svg}</svg>`)));
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 {string} content
* @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(content, flags) {
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
var fn = template(`<svg>${content}</svg>`, 0); // we don't need to worry about using importNode for SVGs
/** @type {Element | DocumentFragment} */
var node;
return () => {
if (cached_content === undefined) {
const content = /** @type {Node} */ (child(create_fragment_from_html(`<svg>${svg}</svg>`)));
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) {
var svg = /** @type {Element} */ (fn());
if ((flags & TEMPLATE_FRAGMENT) === 0) {
node = /** @type {Element} */ (svg.firstChild);
} else {
node = document.createDocumentFragment();
while (svg.firstChild) {
node.appendChild(svg.firstChild);
}
}
}
return clone_node(node, true);
};
}
/**
* @param {boolean} is_fragment
* @param {boolean} use_clone_node
* @param {() => Node} [template_element_fn]
* @returns {Element | DocumentFragment | Node[]}
* @param {string} content
* @param {number} flags
* @returns {() => Node | 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]);
}
export function svg_template_with_script(content, flags) {
var first = true;
var fn = svg_template(content, flags);
return use_clone_node
? clone_node(/** @type {() => Element} */ (template_element_fn)(), true)
: document.importNode(/** @type {() => Element} */ (template_element_fn)(), true);
}
return () => {
if (hydrating) return fn();
/**
* @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));
var node = /** @type {Element | DocumentFragment} */ (fn());
if (first) {
first = false;
run_scripts(node);
}
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,49 +150,28 @@ 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
* and insert the elements into the dom (in client mode).
* @param {import('#client').Dom} dom
* @param {boolean} is_fragment
* @param {Text | Comment | Element} anchor
* @returns {import('#client').Dom}
* @param {import('#client').Dom} dom
*/
function close_template(dom, is_fragment, anchor) {
export function append(anchor, dom) {
var current = dom;
if (!hydrating) {
if (is_fragment) {
var node = /** @type {Node} */ (dom);
if (node.nodeType === 11) {
// if hydrating, `dom` is already an array of nodes, but if not then
// we need to create an array to store it on the current effect
current = /** @type {import('#client').Dom} */ ([.../** @type {Node} */ (dom).childNodes]);
current = /** @type {import('#client').Dom} */ ([...node.childNodes]);
}
// TODO ideally we'd do `anchor.before(dom)`, but that fails because `dom` can be an array of nodes in the SVG case
insert(current, anchor);
anchor.before(node);
}
/** @type {import('#client').Effect} */ (current_effect).dom = current;
return current;
}
/**
* @param {Text | Comment | Element} anchor
* @param {Element | Text} dom
*/
export function close(anchor, dom) {
return close_template(dom, false, anchor);
}
/**
* @param {Text | Comment | Element} anchor
* @param {Element | Text} dom
*/
export function close_frag(anchor, dom) {
return close_template(dom, true, anchor);
}

@ -11,6 +11,6 @@ export default function Bind_this($$anchor, $$props) {
var node = $.first_child(fragment);
$.bind_this(Foo(node, {}), ($$value) => foo = $$value, () => foo);
$.close_frag($$anchor, fragment);
$.append($$anchor, fragment);
$.pop();
}

@ -3,7 +3,7 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal";
var frag = $.template(`<div></div> <svg></svg> <custom-element></custom-element> <div></div> <svg></svg> <custom-element></custom-element>`, true);
var root = $.template(`<div></div> <svg></svg> <custom-element></custom-element> <div></div> <svg></svg> <custom-element></custom-element>`, 3);
export default function Main($$anchor, $$props) {
$.push($$props, true);
@ -11,7 +11,7 @@ export default function Main($$anchor, $$props) {
// needs to be a snapshot test because jsdom does auto-correct the attribute casing
let x = 'test';
let y = () => 'test';
var fragment = $.open_frag(frag, false);
var fragment = root();
var div = $.first_child(fragment);
var svg = $.sibling($.sibling(div, true));
var custom_element = $.sibling($.sibling(svg, true));
@ -33,6 +33,6 @@ export default function Main($$anchor, $$props) {
$.set_custom_element_data(custom_element, "fooBar", x);
});
$.close_frag($$anchor, fragment);
$.append($$anchor, fragment);
$.pop();
}

@ -14,9 +14,9 @@ export default function Each_string_template($$anchor, $$props) {
var text = $.text($$anchor);
$.render_effect(() => $.set_text(text, `${$.stringify($.unwrap(thing))}, `));
return $.close($$anchor, text);
$.append($$anchor, text);
});
$.close_frag($$anchor, fragment);
$.append($$anchor, fragment);
$.pop();
}

@ -24,10 +24,10 @@ export default function Function_prop_no_getter($$anchor, $$props) {
var text = $.text($$anchor);
$.render_effect(() => $.set_text(text, `clicks: ${$.stringify($.get(count))}`));
return $.close($$anchor, text);
$.append($$anchor, text);
}
});
$.close_frag($$anchor, fragment);
$.append($$anchor, fragment);
$.pop();
}

@ -3,14 +3,14 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal";
var frag = $.template(`<h1>hello world</h1>`);
var root = $.template(`<h1>hello world</h1>`);
export default function Hello_world($$anchor, $$props) {
$.push($$props, false);
$.init();
var h1 = $.open(frag);
var h1 = root();
$.close($$anchor, h1);
$.append($$anchor, h1);
$.pop();
}

@ -10,14 +10,14 @@ function reset(_, str, tpl) {
$.set(tpl, ``);
}
var frag = $.template(`<input> <input> <button>reset</button>`, true);
var root = $.template(`<input> <input> <button>reset</button>`, 1);
export default function State_proxy_literal($$anchor, $$props) {
$.push($$props, true);
let str = $.source('');
let tpl = $.source(``);
var fragment = $.open_frag(frag);
var fragment = root();
var input = $.first_child(fragment);
$.remove_input_attr_defaults(input);
@ -31,7 +31,7 @@ export default function State_proxy_literal($$anchor, $$props) {
button.__click = [reset, str, tpl];
$.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value));
$.bind_value(input_1, () => $.get(tpl), ($$value) => $.set(tpl, $$value));
$.close_frag($$anchor, fragment);
$.append($$anchor, fragment);
$.pop();
}

@ -11,6 +11,6 @@ export default function Svelte_element($$anchor, $$props) {
var node = $.first_child(fragment);
$.element(node, tag, false);
$.close_frag($$anchor, fragment);
$.append($$anchor, fragment);
$.pop();
}
Loading…
Cancel
Save