Repository: sveltejs/svelte (JavaScript)
Date: 2018-02-24 17:15:47+00:00
Task type: Bug Fix

Issue:
`text is undefined` error with hydrated slotted component with default slot contents using bound computed properties
The setup is as convoluted as the title suggests, ha. I'm trying to do something pretty stupid, so I'm not exactly surprised that things break, but I found it interesting it seems to happen only when all of these conditions are met.

Essentially, I'm trying to duplicate Vue's [scoped slots](https://vuejs.org/v2/guide/components.html#Scoped-Slots) using a bound property. The parent renders a slotted component, passing in data and receiving some transformed data back using a bound property. The slotted component has some default content which is being overridden from the parent, but both contents are rendered using the same computed value.

This all works in a normal component, but it breaks when the component is hydrated after being server rendered. As such, if using Sapper you navigate to the page, everything works, but if you reload the page it breaks. Additionally, if the slotted child doesn't have any default content, everything works fine as well.

[Repro repo](https://github.com/jacobmischka/svelte-hydrate-slot)

![peek 2018-02-01 22-48](https://user-images.githubusercontent.com/3939997/35716994-05d2b5fc-07a2-11e8-851f-f9ec77bbc0ff.gif)

(That HMR error is unrelated, the same thing happens when running in production mode).

Parent:

```html
<Layout page='about'>
	<Slotted num="{{num}}" bind:computedNum="computedNum">
		<p style="color: red;">
			{{computedNum}}
		</p>
	</Slotted>
</Layout>

<script>
	import Layout from './_components/Layout.html';
	import Slotted from './_components/Slotted.html';

	export default {
		data() {
			return {
				num: 1
			};
		},
		components: {
			Layout,
			Slotted
		}
	};
</script>
```

Slotted child:

```html
<select bind:value="someState">
	<option>a</option>
	<option>b</option>
	<option>c</option>
</select>

<slot>
	<span>{{computedNum}}</span>
</slot>

{{someState}}

<script>
	export default {
		data() {
			return {
				someState: 'a'
			};
		},
		computed: {
			computedNum(num, someState) {
				if (!num)
					return;

				return `${someState}:${num}`;
			}
		}
	};
</script>
```

Thanks!

`text is undefined` error with hydrated slotted component with default slot contents using bound computed properties
The setup is as convoluted as the title suggests, ha. I'm trying to do something pretty stupid, so I'm not exactly surprised that things break, but I found it interesting it seems to happen only when all of these conditions are met.

Essentially, I'm trying to duplicate Vue's [scoped slots](https://vuejs.org/v2/guide/components.html#Scoped-Slots) using a bound property. The parent renders a slotted component, passing in data and receiving some transformed data back using a bound property. The slotted component has some default content which is being overridden from the parent, but both contents are rendered using the same computed value.

This all works in a normal component, but it breaks when the component is hydrated after being server rendered. As such, if using Sapper you navigate to the page, everything works, but if you reload the page it breaks. Additionally, if the slotted child doesn't have any default content, everything works fine as well.

[Repro repo](https://github.com/jacobmischka/svelte-hydrate-slot)

![peek 2018-02-01 22-48](https://user-images.githubusercontent.com/3939997/35716994-05d2b5fc-07a2-11e8-851f-f9ec77bbc0ff.gif)

(That HMR error is unrelated, the same thing happens when running in production mode).

Parent:

```html
<Layout page='about'>
	<Slotted num="{{num}}" bind:computedNum="computedNum">
		<p style="color: red;">
			{{computedNum}}
		</p>
	</Slotted>
</Layout>

<script>
	import Layout from './_components/Layout.html';
	import Slotted from './_components/Slotted.html';

	export default {
		data() {
			return {
				num: 1
			};
		},
		components: {
			Layout,
			Slotted
		}
	};
</script>
```

Slotted child:

```html
<select bind:value="someState">
	<option>a</option>
	<option>b</option>
	<option>c</option>
</select>

<slot>
	<span>{{computedNum}}</span>
</slot>

{{someState}}

<script>
	export default {
		data() {
			return {
				someState: 'a'
			};
		},
		computed: {
			computedNum(num, someState) {
				if (!num)
					return;

				return `${someState}:${num}`;
			}
		}
	};
</script>
```

Thanks!



Issue discussion:
Finally taking a poke at this — it's a hell of a bug. Think I've whittled it down to [this](https://svelte.technology/repl?version=1.55.0&gist=0fde0f660ffbe7a4eeb249d77c0333e0) — if the default slotted content has anything dynamic, it causes problems.

Your repro was using Sapper 0.5.1 which actually got hydrating backwards (the only time it *didn't* hydrate was on the first load), so this isn't a hydration bug per se.

Closing in on a fix...
Finally taking a poke at this — it's a hell of a bug. Think I've whittled it down to [this](https://svelte.technology/repl?version=1.55.0&gist=0fde0f660ffbe7a4eeb249d77c0333e0) — if the default slotted content has anything dynamic, it causes problems.

Your repro was using Sapper 0.5.1 which actually got hydrating backwards (the only time it *didn't* hydrate was on the first load), so this isn't a hydration bug per se.

Closing in on a fix...

Solution patch:
diff --git a/src/generators/nodes/Slot.ts b/src/generators/nodes/Slot.ts
--- a/src/generators/nodes/Slot.ts
+++ b/src/generators/nodes/Slot.ts
@@ -57,6 +57,7 @@ export default class Slot extends Element {
 		block.builders.create.pushCondition(`!${content_name}`);
 		block.builders.hydrate.pushCondition(`!${content_name}`);
 		block.builders.mount.pushCondition(`!${content_name}`);
+		block.builders.update.pushCondition(`!${content_name}`);
 		block.builders.unmount.pushCondition(`!${content_name}`);
 		block.builders.destroy.pushCondition(`!${content_name}`);
 
@@ -67,6 +68,7 @@ export default class Slot extends Element {
 		block.builders.create.popCondition();
 		block.builders.hydrate.popCondition();
 		block.builders.mount.popCondition();
+		block.builders.update.popCondition();
 		block.builders.unmount.popCondition();
 		block.builders.destroy.popCondition();
 


Test patch:
diff --git a/test/runtime/samples/component-slot-dynamic/Nested.html b/test/runtime/samples/component-slot-dynamic/Nested.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-dynamic/Nested.html
@@ -0,0 +1,13 @@
+<slot>
+	<span>{{foo}}</span>
+</slot>
+
+<script>
+	export default {
+		data() {
+			return {
+				foo: 'a'
+			};
+		}
+	};
+</script>
diff --git a/test/runtime/samples/component-slot-dynamic/_config.js b/test/runtime/samples/component-slot-dynamic/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-dynamic/_config.js
@@ -0,0 +1,9 @@
+export default {
+	html: `
+		<p>override default slot</p>
+	`,
+
+	test(assert, component) {
+		component.refs.nested.set({ foo: 'b' });
+	}
+};
diff --git a/test/runtime/samples/component-slot-dynamic/main.html b/test/runtime/samples/component-slot-dynamic/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-dynamic/main.html
@@ -0,0 +1,13 @@
+<Nested ref:nested>
+	<p>override default slot</p>
+</Nested>
+
+<script>
+	import Nested from './Nested.html';
+
+	export default {
+		components: {
+			Nested
+		}
+	};
+</script>