Repository: sveltejs/svelte (JavaScript)
Date: 2021-07-22 06:29:51+00:00
Task type: Refactoring

Issue:
`svg_element` function is output even if no svg element in a component
### Describe the problem

when `hydratable: true`, `svg_element` function is output even if no svg element in a component.

For example, if you have a simple component like the following:

```html
<main>
	<h1>Hello world!</h1>
</main>
```

compiled:
```js
  function svg_element(name) {
    return document.createElementNS("http://www.w3.org/2000/svg", name);
  }

  ...

  function claim_element(nodes, name, attributes, svg) {
    return claim_node(
      nodes,
      (node) => node.nodeName === name,
      (node) => {
        const remove = [];
        for (let j = 0; j < node.attributes.length; j++) {
          const attribute = node.attributes[j];
          if (!attributes[attribute.name]) {
            remove.push(attribute.name);
          }
        }
        remove.forEach((v) => node.removeAttribute(v));
      },
      () => (svg ? svg_element(name) : element(name))
    );
  }
```

minified(formatted):
<pre>
  function m(t, n, e, o) {
    return _(
      t,
      (t) => t.nodeName === n,
      (t) => {
        const n = [];
        for (let o = 0; o < t.attributes.length; o++) {
          const r = t.attributes[o];
          e[r.name] || n.push(r.name);
        }
        n.forEach((n) => t.removeAttribute(n));
      },
      () =>
        o
          ?<b> (function (t) {
              return document.createElementNS("http://www.w3.org/2000/svg", t);
            })(n)</b>
          : s(n)
    );
  }
</pre>

This isn't a bug (because it works fine), but it does increase the bundle size slightly.

### Describe the proposed solution

Pass `svg_element` function, instead of `1`.

- src/compiler/compile/render_dom/wrappers/Element/index.ts

```diff
	get_claim_statement(nodes: Identifier) {
		const attributes = this.attributes
			.filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name)
			.map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`);

		const name = this.node.namespace
			? this.node.name
			: this.node.name.toUpperCase();

-		const svg = this.node.namespace === namespaces.svg ? 1 : null;
+		const svg = this.node.namespace === namespaces.svg ? '@svg_element' : null;

		return x`@claim_element(${nodes}, "${name}", { ${attributes} }, ${svg})`;
	}
```



- src/runtime/internal/dom.ts

```diff
-	export function claim_element(nodes: ChildNodeArray, name: string, attributes: { [key: string]: boolean }, svg) {
+	export function claim_element(nodes: ChildNodeArray, name: string, attributes: { [key: string]: boolean }, create_element: (name: string) => Element | SVGElement = element) {
		return claim_node<Element | SVGElement>(
			nodes,
			(node: ChildNode): node is Element | SVGElement => node.nodeName === name,
			(node: Element) => {
				const remove = [];
				for (let j = 0; j < node.attributes.length; j++) {
					const attribute = node.attributes[j];
					if (!attributes[attribute.name]) {
						remove.push(attribute.name);
					}
				}
				remove.forEach(v => node.removeAttribute(v));
				return undefined;
			},
-			() => svg ? svg_element(name as keyof SVGElementTagNameMap) : element(name as keyof HTMLElementTagNameMap)
+			() => create_element(name)
		);
	}
```



### Alternatives considered

Also, `element` function is output even if the component has only svg elements.
If it is important to improve that as well, more changes are needed.

### Importance

nice to have


Issue discussion:

Solution patch:
diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts
--- a/src/compiler/compile/render_dom/wrappers/Element/index.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts
@@ -388,9 +388,11 @@ export default class ElementWrapper extends Wrapper {
 			? this.node.name
 			: this.node.name.toUpperCase();
 
-		const svg = this.node.namespace === namespaces.svg ? 1 : null;
-
-		return x`@claim_element(${nodes}, "${name}", { ${attributes} }, ${svg})`;
+		if (this.node.namespace === namespaces.svg) {
+			return x`@claim_svg_element(${nodes}, "${name}", { ${attributes} })`;
+		} else {
+			return x`@claim_element(${nodes}, "${name}", { ${attributes} })`;
+		}
 	}
 
 	add_directives_in_order (block: Block) {
diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts
--- a/src/runtime/internal/dom.ts
+++ b/src/runtime/internal/dom.ts
@@ -433,7 +433,7 @@ function claim_node<R extends ChildNodeEx>(nodes: ChildNodeArray, predicate: (no
 	return resultNode;
 }
 
-export function claim_element(nodes: ChildNodeArray, name: string, attributes: {[key: string]: boolean}, svg) {
+function claim_element_base(nodes: ChildNodeArray, name: string, attributes: { [key: string]: boolean }, create_element: (name: string) => Element | SVGElement) {
 	return claim_node<Element | SVGElement>(
 		nodes,
 		(node: ChildNode): node is Element | SVGElement => node.nodeName === name,
@@ -448,10 +448,18 @@ export function claim_element(nodes: ChildNodeArray, name: string, attributes: {
 			remove.forEach(v => node.removeAttribute(v));
 			return undefined;
 		},
-		() => svg ? svg_element(name as keyof SVGElementTagNameMap) : element(name as keyof HTMLElementTagNameMap)
+		() => create_element(name)
 	);
 }
 
+export function claim_element(nodes: ChildNodeArray, name: string, attributes: { [key: string]: boolean }) {
+	return claim_element_base(nodes, name, attributes, element);
+}
+
+export function claim_svg_element(nodes: ChildNodeArray, name: string, attributes: { [key: string]: boolean }) {
+	return claim_element_base(nodes, name, attributes, svg_element);
+}
+
 export function claim_text(nodes: ChildNodeArray, data) {
 	return claim_node<Text>(
 		nodes,


Test patch:
diff --git a/test/js/samples/hydrated-void-svg-element/_config.js b/test/js/samples/hydrated-void-svg-element/_config.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/hydrated-void-svg-element/_config.js
@@ -0,0 +1,5 @@
+export default {
+	options: {
+		hydratable: true
+	}
+};
diff --git a/test/js/samples/hydrated-void-svg-element/expected.js b/test/js/samples/hydrated-void-svg-element/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/hydrated-void-svg-element/expected.js
@@ -0,0 +1,58 @@
+/* generated by Svelte vX.Y.Z */
+import {
+	SvelteComponent,
+	append_hydration,
+	children,
+	claim_svg_element,
+	claim_text,
+	detach,
+	init,
+	insert_hydration,
+	noop,
+	safe_not_equal,
+	svg_element,
+	text
+} from "svelte/internal";
+
+function create_fragment(ctx) {
+	let svg;
+	let title;
+	let t;
+
+	return {
+		c() {
+			svg = svg_element("svg");
+			title = svg_element("title");
+			t = text("a title");
+		},
+		l(nodes) {
+			svg = claim_svg_element(nodes, "svg", {});
+			var svg_nodes = children(svg);
+			title = claim_svg_element(svg_nodes, "title", {});
+			var title_nodes = children(title);
+			t = claim_text(title_nodes, "a title");
+			title_nodes.forEach(detach);
+			svg_nodes.forEach(detach);
+		},
+		m(target, anchor) {
+			insert_hydration(target, svg, anchor);
+			append_hydration(svg, title);
+			append_hydration(title, t);
+		},
+		p: noop,
+		i: noop,
+		o: noop,
+		d(detaching) {
+			if (detaching) detach(svg);
+		}
+	};
+}
+
+class Component extends SvelteComponent {
+	constructor(options) {
+		super();
+		init(this, options, null, create_fragment, safe_not_equal, {});
+	}
+}
+
+export default Component;
diff --git a/test/js/samples/hydrated-void-svg-element/input.svelte b/test/js/samples/hydrated-void-svg-element/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/js/samples/hydrated-void-svg-element/input.svelte
@@ -0,0 +1,3 @@
+<svg>
+	<title>a title</title>
+</svg>
\ No newline at end of file