Repository: sveltejs/svelte (JavaScript)
Date: 2021-07-27 09:05:45+00:00
Task type: Bug Fix

Issue:
[svelte-native] broken custom components with 'src' attribute
### Describe the bug

This commit https://github.com/sveltejs/svelte/commit/ecbd96af95eddf7d53d24fa78aeb5818f2cf71b9 broke [svelte-native](https://svelte-native.technology/).
The reason is that we dont have `a` element.
I think the handle of `src` attribute should not happen on `foreign` namespace.

### Reproduction

create a simple `svelte-native` project with a listview and `image` component (which has a `src` attribute).
It will crash as you scroll with that error:
```
System.err: Calling js method onBindViewHolder failed
System.err: TypeError: No known component for element a.
System.err: 
System.err: StackTrace:
System.err: createElement(file: node_modules/.pnpm/svelte-native@0.9.7_6638bbb868bf526fdb8641bdfec1676a/node_modules/svelte-native/dom/index.js:321:0)
```

### Logs

_No response_

### System Info

```shell
Nativescript 8.x
Svelte 3.40.2
```


### Severity

blocking an upgrade


Issue discussion:
Yeah this sounds reasonable. I think it would make sense to add a check to https://github.com/sveltejs/svelte/blob/71c29bbf3599a93d55667aa5acff1675fd395679/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts#L133 that `this.parent.node.namespace !== namespaces.foreign`.
@Conduitry i was wondering if should not even put it in the `is_src` setter.  I am afraid it might happen again in the future from other places. Ensuring that `is_src` is always false in svelte-native  could enforce it never happens again.

Yeah that would be another possibility. I don't have particularly strong opinions one way or the other. It looks like the only other thing that would currently affect would be `should_cache`, which might actually be desirable.
Hi.

I fixed this issue.
But I want to confirm what I did is correct or not.

## What I did

Already @farfromrefug mentioned that I can fix it after modifying the way of assign value to `is_src` like below.

https://github.com/sveltejs/svelte/blob/71c29bbf3599a93d55667aa5acff1675fd395679/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts#L90

```
this.is_src = this.name === 'src' && this.parent.node.namespace !== namespaces.foreign;
```
 
## Result

But after fixing up, still, an error occurred in the svelte native.
Because `is_src` assinged true if parent node name is `<image>` tag and `namespace` is `http://www.w3.org/2000/svg`.

## My Opinion

I think `this.is_src` should be true if an attribute name is `src` and namespaces is only HTML(`http://www.w3.org/1999/xhtml`).
But I don't have confidence due to my knowledge.
Therefore I want to confirm what I did is correct or not.

```
this.is_src = this.name === 'src' && (!this.parent.node.namespace || this.parent.node.namespace === namespaces.html);
```

Notice:  Already I executed all existing tests after change the code and these were passed.

## Survey MEMO

I've investigated the possibility that `this.is_src` may be assigned `true` by each namespace.

- foreign (`https://svelte.dev/docs#svelte_options`)

It comes if code is like `<svelte:options tag="my-custom-element"/>`.
Always `this.is_src` should be false.

- html (`http://www.w3.org/1999/xhtml`)
This is normal HTML. 
`this.is_src` should be true if attribute name is `src`.

- mathml (`http://www.w3.org/1998/Math/MathML`)
We can't judge `src` attribute should come URL.
So always `this.is_src` should be false.

- svg (`http://www.w3.org/2000/svg`)
refer: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image
If use `<image>` inside `<svg>`, should use `href` attribute instead of `src`.
So always `this.is_src` should be false.


- xlink (`http://www.w3.org/1999/xlink`)
This is XML (not HTML).
We can't judge `src` attribute should come URL.
So always `this.is_src` should be false.

- xml (`http://www.w3.org/XML/1998/namespace`)
This is XML (not HTML).
We can't judge `src` attribute should come URL.
So always `this.is_src` should be false.

- xmlns  (http://www.w3.org/2000/xmlns`)
We can't judge `src` attribute should come URL.
So always `this.is_src` should be false.


Thank you for reading!🎈

Solution patch:
diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
--- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
@@ -86,8 +86,9 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
 			this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
 			this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
 		}
-		
-		this.is_src = this.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
+
+		// TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
+		this.is_src = this.name === 'src' && (!this.parent.node.namespace || this.parent.node.namespace === namespaces.html);
 		this.should_cache = should_cache(this);
 	}
 
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
@@ -148,7 +148,7 @@ export function get_root_for_style(node: Node): ShadowRoot | Document {
 	if (!node) return document;
 
 	const root = node.getRootNode ? node.getRootNode() : node.ownerDocument;
-	if ((root as ShadowRoot).host) {
+	if (root && (root as ShadowRoot).host) {
 		return root as ShadowRoot;
 	}
 	return document;


Test patch:
diff --git a/test/js/samples/src-attribute-check-in-foreign/_config.js b/test/js/samples/src-attribute-check-in-foreign/_config.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/src-attribute-check-in-foreign/_config.js
@@ -0,0 +1,5 @@
+export default {
+	options: {
+		hydratable: true
+	}
+};
diff --git a/test/js/samples/src-attribute-check-in-foreign/expected.js b/test/js/samples/src-attribute-check-in-foreign/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/src-attribute-check-in-foreign/expected.js
@@ -0,0 +1,70 @@
+/* generated by Svelte vX.Y.Z */
+import {
+	SvelteComponent,
+	append_hydration,
+	attr,
+	children,
+	claim_element,
+	detach,
+	init,
+	insert_hydration,
+	noop,
+	safe_not_equal
+} from "svelte/internal";
+
+function create_fragment(ctx) {
+	let svg;
+	let img;
+
+	return {
+		c() {
+			svg = document.createElementNS("https://svelte.dev/docs#svelte_options", "svg");
+			img = document.createElementNS("https://svelte.dev/docs#svelte_options", "img");
+			this.h();
+		},
+		l(nodes) {
+			svg = claim_element(nodes, "svg", {});
+			var svg_nodes = children(svg);
+			img = claim_element(svg_nodes, "img", { alt: true, src: true });
+			svg_nodes.forEach(detach);
+			this.h();
+		},
+		h() {
+			attr(img, "alt", "potato");
+			attr(img, "src", /*url*/ ctx[0]);
+		},
+		m(target, anchor) {
+			insert_hydration(target, svg, anchor);
+			append_hydration(svg, img);
+		},
+		p(ctx, [dirty]) {
+			if (dirty & /*url*/ 1) {
+				attr(img, "src", /*url*/ ctx[0]);
+			}
+		},
+		i: noop,
+		o: noop,
+		d(detaching) {
+			if (detaching) detach(svg);
+		}
+	};
+}
+
+function instance($$self, $$props, $$invalidate) {
+	let { url } = $$props;
+
+	$$self.$$set = $$props => {
+		if ('url' in $$props) $$invalidate(0, url = $$props.url);
+	};
+
+	return [url];
+}
+
+class Component extends SvelteComponent {
+	constructor(options) {
+		super();
+		init(this, options, instance, create_fragment, safe_not_equal, { url: 0 });
+	}
+}
+
+export default Component;
diff --git a/test/js/samples/src-attribute-check-in-foreign/input.svelte b/test/js/samples/src-attribute-check-in-foreign/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/js/samples/src-attribute-check-in-foreign/input.svelte
@@ -0,0 +1,9 @@
+<svelte:options namespace="foreign" />
+
+<script>
+	export let url;
+</script>
+
+<svg>
+	<img alt="potato" src={url}>
+</svg>
diff --git a/test/js/samples/src-attribute-check-in-svg/_config.js b/test/js/samples/src-attribute-check-in-svg/_config.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/src-attribute-check-in-svg/_config.js
@@ -0,0 +1,5 @@
+export default {
+	options: {
+		hydratable: true
+	}
+};
diff --git a/test/js/samples/src-attribute-check-in-svg/expected.js b/test/js/samples/src-attribute-check-in-svg/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/src-attribute-check-in-svg/expected.js
@@ -0,0 +1,71 @@
+/* generated by Svelte vX.Y.Z */
+import {
+	SvelteComponent,
+	append_hydration,
+	attr,
+	children,
+	claim_element,
+	detach,
+	init,
+	insert_hydration,
+	noop,
+	safe_not_equal,
+	svg_element
+} from "svelte/internal";
+
+function create_fragment(ctx) {
+	let svg;
+	let img;
+
+	return {
+		c() {
+			svg = svg_element("svg");
+			img = svg_element("img");
+			this.h();
+		},
+		l(nodes) {
+			svg = claim_element(nodes, "svg", {}, 1);
+			var svg_nodes = children(svg);
+			img = claim_element(svg_nodes, "img", { alt: true, src: true }, 1);
+			svg_nodes.forEach(detach);
+			this.h();
+		},
+		h() {
+			attr(img, "alt", "potato");
+			attr(img, "src", /*url*/ ctx[0]);
+		},
+		m(target, anchor) {
+			insert_hydration(target, svg, anchor);
+			append_hydration(svg, img);
+		},
+		p(ctx, [dirty]) {
+			if (dirty & /*url*/ 1) {
+				attr(img, "src", /*url*/ ctx[0]);
+			}
+		},
+		i: noop,
+		o: noop,
+		d(detaching) {
+			if (detaching) detach(svg);
+		}
+	};
+}
+
+function instance($$self, $$props, $$invalidate) {
+	let { url } = $$props;
+
+	$$self.$$set = $$props => {
+		if ('url' in $$props) $$invalidate(0, url = $$props.url);
+	};
+
+	return [url];
+}
+
+class Component extends SvelteComponent {
+	constructor(options) {
+		super();
+		init(this, options, instance, create_fragment, safe_not_equal, { url: 0 });
+	}
+}
+
+export default Component;
diff --git a/test/js/samples/src-attribute-check-in-svg/input.svelte b/test/js/samples/src-attribute-check-in-svg/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/js/samples/src-attribute-check-in-svg/input.svelte
@@ -0,0 +1,7 @@
+<script>
+	export let url;
+</script>
+
+<svg>
+	<img alt="potato" src={url}>
+</svg>