Repository: sveltejs/svelte (JavaScript)
Date: 2020-03-07 06:40:14+00:00
Task type: Bug Fix

Issue:
Prop initialization in web/standalone components
Today I created a small, self-contained standalone/web-component in svelte and stumbled across the following behaviour:

In my component, i declare a few properties like so:
```javascript
export let zip = null;
export let radius = 10;
```
I then create/bundle the component with rollup, include and use it in my webpage:
```html
<html>
<head>
<script src='bundle.js'></script>
</head>
<body>
<my-component zip="12345" radius="15></my-component>
</body>
```
Then, in the component I wanted to use the defined props in the `onMount`-Hook to set additional parameters for a call to an external API. The result from this call should then be displayed by the component. But at that point in the lifecycle, they are just undefined or have their initial default values. Only after the second, third, n-th `beforeUpdate`-Hook they get their external values passed down, as can be seen on this screenshot:

![image](https://user-images.githubusercontent.com/16416675/54464926-10347900-4779-11e9-8c16-5d9368c3a656.png)

Basically it does one complete lifecycle for each defined prop. I somewhat fixed the problem of the missing values by encapsulating everything in a `$`-block, but that creates subsequent calls to the external API for each prop.
At that point I didn't know if I did something wrong, or if this is somewhat expected for web-components to do. If not, then that should be fixed so that all prop-values are correctly set in `onMount`.

**Versions from package.json:**
    "npm-run-all": "^4.1.5",
    "rollup": "^1.2.2",
    "rollup-plugin-commonjs": "^9.2.0",
    "rollup-plugin-node-resolve": "^4.0.1",
    "rollup-plugin-svelte": "^5.0.3",
    "rollup-plugin-terser": "^4.0.4",
    "sirv-cli": "^0.2.3",
    "svelte": "^3.0.0-beta.7"

Wrapping of buttons in REPL (.app-controls)
Buttons don't appear on the same line in Safari Mobile:

![image](https://user-images.githubusercontent.com/12690/87665502-559af900-c767-11ea-947d-0dd079cc34dc.png)

This is especially apparent when viewing a REPL on iOS on an iPhone 8/iPhone SE.

This be fixed by adding a rule of `white-space: nowrap` on `.app-controls`.

![image](https://user-images.githubusercontent.com/12690/87665509-5764bc80-c767-11ea-81b7-981cedbd4343.png)

I will send a PR referencing this issue number.


Issue discussion:
Update on this, the problem is the execution order of sveltes onMount-handler in relation to the web components `attributeChangedCallback` and `connectedCallback`. I added some `console.log`s in the source code, to get a glimpse on the order of execution. As you can see, onMount gets called first, then the attributeChanged-callbacks and finally connected.

![image](https://user-images.githubusercontent.com/16416675/54595576-32f1b680-4a33-11e9-8d79-795db5459b3d.png)

I think it would make more sense to call `onMount` last in the case that the component is a standalone/web component - if this is possible at all. Tested this with `svelte@3.0.0-beta.20`
Same problem here. Been banging my head against the wall for several hours. I see a prop in the template but can't access it from onMount - it's undefined. Moreover, I get warnings of type `was created without expected prop 'title'`.

Here's my HTML:
`<site-message title="hello"></site-message>`

Here's my Svelte:
```
<script>
  import { onMount } from 'svelte';
  export let title;
  onMount(() => {
    console.log(title); // undefined
  })
</script>

<svelte:options tag="site-message"/>
<div>{title}</div>
```

Does anyone know what might be wrong?
As I mentioned before, it's the load order in which the props get processed in a standalone component. Basically onMount is called too early resulting in undefined props. I also came across the `was created without expected prop`-error, but "fixed" it by giving my props some default values and then checking in my logic if those values were overwritten.

In a nutshell, what I did to fix my problems was 1) ditch onMount in my custom components 2) use `<svelte:window on:load={customOnLoadHandler} />` were `customOnLoadHandler` is just a normal async-function within my component. When this runs, all the props that were set are initialized/overwritten from the defaults and I can access the API I wanted to call, without calling it multiple times. Yes, it is a bit hacky, but it works for now until this is resolved or there is a better solution.
@thepete89 that did a trick, although it feels a bit clumsy. Do you also get warnings of type `was created without expected prop 'title'.` in console.log? The only way to get rid of them is to set default values for props, but this also doesn't feel right.
@timonweb as I wrote in my last comment, I had the same issue and also "fixed" it by giving them default values. Yes, it doesn't feel right, but it's the only way at the moment to get rid of that error.
@thepete89 @timonweb I'm still getting the same issues you guys are getting. 
Was there anything that either of you did to fix it, other than the above described workaround?
@pbastowski nah, I didn't like this workaround and dropped Svelte for now, will wait for better times.
@timonweb I'm also leaning towards not using Svelte to create web-components until this issue is resolved.
The worst issue I encountered is that slots within my Svelte app's components no longer work properly when I wrap it in a web-component. This is a complete deal breaker, unfortunately. Pity.
Feel free to open some PRs to fix the issues you are encountering.
@antony There seems to be a PR present for the slot issue already https://github.com/sveltejs/svelte/pull/3136
@pbastowski nice - have you tried it? can you confirm that it works?
@antony I have just tested it with my test project and it does indeed work as expected.

That is, the slot content from index.html is passed into App.svelte and renders correctly.

App uses Input.svelte, which also has a slot and that slot's contents are rendering as expected.

Based on my limited testing, it would appear that this PR has indeed fixed the broken slot problem.


Nice. I'd recommend you go to the PR and add your findings there / working
code sample there, or maybe just link this issue - as it will speed things
up when it comes to merging it.

Sadly I don't have the access to do so but I know this will be useful to
those who do :)

Thanks

On Sun, 18 Aug 2019 at 10:12, pbastowski <notifications@github.com> wrote:

> @antony <https://github.com/antony> I have just tested it with my test
> project and it does indeed work as expected.
>
> That is, the slot content from index.html is passed into App.svelte and
> renders correctly.
>
> App uses Input.svelte, which also has a slot and that slot's contents are
> rendering as expected.
>
> So, it would appear that this PR has indeed fixed the broken slot problem.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/sveltejs/svelte/issues/2227?email_source=notifications&email_token=AABVORKVPGOCZ3COHPA76UTQFEG67A5CNFSM4G65UU5KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD4Q32YI#issuecomment-522304865>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AABVORLISXDQHKF5FXOOXF3QFEG67ANCNFSM4G65UU5A>
> .
>
-- 
________________________________
ꜽ . antony jones . http://www.enzy.org

@antony done
@thepete89 Apologies for hijacking this issue with slot problems. This issue was originally discussing prop initialization problems, which are still there.

@Rich-Harris I contacted you last week regarding issues with Svelte components wrapped in a web-component. I noticed you were on Twitter recently asking about web-components, so, perhaps it is a good time to revisit this issue as well? I was going to create a new issue, but this one does the job perfectly.
@pbastowski no problem, was not around until yesterday so I had no time to answer your question. svelte:window and default values was the only thing I did to fix the issue with the prop-initialisation. 

Funny thing is, if my webcomponent gets injected asynchronously (say via jQuery oder other DOM-Manipulations in vanilla JS) then i HAVE to use onMount because `svelte:window on:load` won't work in that case. So it would really make things easier if onMount would behave like it should in either case.


Solution patch:
diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts
--- a/src/compiler/compile/render_dom/index.ts
+++ b/src/compiler/compile/render_dom/index.ts
@@ -485,7 +485,7 @@ export default function dom(
 
 					${css.code && b`this.shadowRoot.innerHTML = \`<style>${css.code.replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
 
-					@init(this, { target: this.shadowRoot, props: ${init_props} }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${dirty});
+					@init(this, { target: this.shadowRoot, props: ${init_props}, customElement: true }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${dirty});
 
 					${dev_props_check}
 
diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts
--- a/src/runtime/internal/Component.ts
+++ b/src/runtime/internal/Component.ts
@@ -34,6 +34,7 @@ interface T$$ {
 	on_mount: any[];
 	on_destroy: any[];
 	skip_bound: boolean;
+	on_disconnect: any[];
 }
 
 export function bind(component, name, callback) {
@@ -52,23 +53,26 @@ export function claim_component(block, parent_nodes) {
 	block && block.l(parent_nodes);
 }
 
-export function mount_component(component, target, anchor) {
+export function mount_component(component, target, anchor, customElement) {
 	const { fragment, on_mount, on_destroy, after_update } = component.$$;
 
 	fragment && fragment.m(target, anchor);
 
-	// onMount happens before the initial afterUpdate
-	add_render_callback(() => {
-		const new_on_destroy = on_mount.map(run).filter(is_function);
-		if (on_destroy) {
-			on_destroy.push(...new_on_destroy);
-		} else {
-			// Edge case - component was destroyed immediately,
-			// most likely as a result of a binding initialising
-			run_all(new_on_destroy);
-		}
-		component.$$.on_mount = [];
-	});
+	if (!customElement) {
+		// onMount happens before the initial afterUpdate
+		add_render_callback(() => {
+
+			const new_on_destroy = on_mount.map(run).filter(is_function);
+			if (on_destroy) {
+				on_destroy.push(...new_on_destroy);
+			} else {
+				// Edge case - component was destroyed immediately,
+				// most likely as a result of a binding initialising
+				run_all(new_on_destroy);
+			}
+			component.$$.on_mount = [];
+		});
+	}
 
 	after_update.forEach(add_render_callback);
 }
@@ -113,6 +117,7 @@ export function init(component, options, instance, create_fragment, not_equal, p
 		// lifecycle
 		on_mount: [],
 		on_destroy: [],
+		on_disconnect: [],
 		before_update: [],
 		after_update: [],
 		context: new Map(parent_component ? parent_component.$$.context : []),
@@ -155,7 +160,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);
+		mount_component(component, options.target, options.anchor, options.customElement);
 		flush();
 	}
 
@@ -173,6 +178,9 @@ if (typeof HTMLElement === 'function') {
 		}
 
 		connectedCallback() {
+			const { on_mount } = this.$$;
+			this.$$.on_disconnect = on_mount.map(run).filter(is_function);
+
 			// @ts-ignore todo: improve typings
 			for (const key in this.$$.slotted) {
 				// @ts-ignore todo: improve typings
@@ -184,6 +192,10 @@ if (typeof HTMLElement === 'function') {
 			this[attr] = newValue;
 		}
 
+		disconnectedCallback() {
+			run_all(this.$$.on_disconnect);
+		}
+
 		$destroy() {
 			destroy_component(this, 1);
 			this.$destroy = noop;


Test patch:
diff --git a/test/custom-elements/index.ts b/test/custom-elements/index.ts
--- a/test/custom-elements/index.ts
+++ b/test/custom-elements/index.ts
@@ -110,8 +110,8 @@ describe('custom-elements', function() {
 
 			const page = await browser.newPage();
 
-			page.on('console', (type, ...args) => {
-				console[type](...args);
+			page.on('console', (type) => {
+				console[type._type](type._text);
 			});
 
 			page.on('error', error => {
diff --git a/test/custom-elements/samples/oncreate/main.svelte b/test/custom-elements/samples/oncreate/main.svelte
--- a/test/custom-elements/samples/oncreate/main.svelte
+++ b/test/custom-elements/samples/oncreate/main.svelte
@@ -3,9 +3,12 @@
 <script>
 	import { onMount } from 'svelte';
 
-	export let wasCreated;
+  export let prop = false;
+  export let propsInitialized;
+  export let wasCreated;
 
-	onMount(() => {
-		wasCreated = true;
-	});
+  onMount(() => {
+    propsInitialized = prop !== false;
+    wasCreated = true;
+  });
 </script>
diff --git a/test/custom-elements/samples/oncreate/test.js b/test/custom-elements/samples/oncreate/test.js
--- a/test/custom-elements/samples/oncreate/test.js
+++ b/test/custom-elements/samples/oncreate/test.js
@@ -2,7 +2,9 @@ import * as assert from 'assert';
 import './main.svelte';
 
 export default function (target) {
-	target.innerHTML = '<my-app/>';
+	target.innerHTML = '<my-app prop/>';
 	const el = target.querySelector('my-app');
+
 	assert.ok(el.wasCreated);
+	assert.ok(el.propsInitialized);
 }
diff --git a/test/custom-elements/samples/ondestroy/main.svelte b/test/custom-elements/samples/ondestroy/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/custom-elements/samples/ondestroy/main.svelte
@@ -0,0 +1,22 @@
+<svelte:options tag="my-app"/>
+
+<script>
+	import { onMount, onDestroy } from 'svelte';
+	
+	let el;
+	let parentEl;
+
+	onMount(() => {
+		parentEl = el.parentNode.host.parentElement;
+
+		return () => {
+			parentEl.dataset.onMountDestroyed = true;
+		}
+	});
+
+	onDestroy(() => {
+		parentEl.dataset.destroyed = true;
+	})
+</script>
+
+<div bind:this={el}></div>
diff --git a/test/custom-elements/samples/ondestroy/test.js b/test/custom-elements/samples/ondestroy/test.js
new file mode 100644
--- /dev/null
+++ b/test/custom-elements/samples/ondestroy/test.js
@@ -0,0 +1,11 @@
+import * as assert from 'assert';
+import './main.svelte';
+
+export default function (target) {
+	target.innerHTML = '<my-app/>';
+	const el = target.querySelector('my-app');
+	target.removeChild(el);
+
+	assert.ok(target.dataset.onMountDestroyed);
+	assert.equal(target.dataset.destroyed, undefined);
+}
diff --git a/test/js/samples/css-shadow-dom-keyframes/expected.js b/test/js/samples/css-shadow-dom-keyframes/expected.js
--- a/test/js/samples/css-shadow-dom-keyframes/expected.js
+++ b/test/js/samples/css-shadow-dom-keyframes/expected.js
@@ -40,7 +40,8 @@ class Component extends SvelteElement {
 			this,
 			{
 				target: this.shadowRoot,
-				props: attribute_to_object(this.attributes)
+				props: attribute_to_object(this.attributes),
+				customElement: true
 			},
 			null,
 			create_fragment,