Repository: sveltejs/svelte (JavaScript)
Date: 2018-05-03 20:15:30+00:00
Task type: Bug Fix

Issue:
Spread props on dynamic components fail if no initial value
Bit of an edge case here...

If you have a `<svelte:component>` whose `this` doesn't have a value, and it has spread properties, then any updates will fail because the initial spread 'levels' have not been set. [REPL](https://svelte.technology/repl?version=2.4.3&gist=0a0598faa7f6549248455e3a7ec67007)

```html
<button on:click="updateProps()">click me</button>
<svelte:component this={undefined} {...props}/>

{#if error}
  <pre>{error.stack}</pre>
{/if}

<script>
  export default {
    data() {
      return {
        props: {}
      };
    },
    
    methods: {
      updateProps() {
        try {
          this.set({ props: {} });
        } catch (error) {
          this.set({ error });
        }
      }
    }
  };
</script>
```

Interestingly, the `this.set({ error })` in that example *also* fails, because always Svelte tries to recalculate spread levels, even if nothing has changed. Should also get fixed.
Spread props on dynamic components fail if no initial value
Bit of an edge case here...

If you have a `<svelte:component>` whose `this` doesn't have a value, and it has spread properties, then any updates will fail because the initial spread 'levels' have not been set. [REPL](https://svelte.technology/repl?version=2.4.3&gist=0a0598faa7f6549248455e3a7ec67007)

```html
<button on:click="updateProps()">click me</button>
<svelte:component this={undefined} {...props}/>

{#if error}
  <pre>{error.stack}</pre>
{/if}

<script>
  export default {
    data() {
      return {
        props: {}
      };
    },
    
    methods: {
      updateProps() {
        try {
          this.set({ props: {} });
        } catch (error) {
          this.set({ error });
        }
      }
    }
  };
</script>
```

Interestingly, the `this.set({ error })` in that example *also* fails, because always Svelte tries to recalculate spread levels, even if nothing has changed. Should also get fixed.


Issue discussion:

Solution patch:
diff --git a/src/compile/nodes/Component.ts b/src/compile/nodes/Component.ts
--- a/src/compile/nodes/Component.ts
+++ b/src/compile/nodes/Component.ts
@@ -16,6 +16,7 @@ import Binding from './Binding';
 import EventHandler from './EventHandler';
 import Expression from './shared/Expression';
 import { AppendTarget } from '../../interfaces';
+import addToSet from '../../utils/addToSet';
 
 export default class Component extends Node {
 	type: 'Component';
@@ -161,10 +162,16 @@ export default class Component extends Node {
 				const initialProps = [];
 				const changes = [];
 
+				const allDependencies = new Set();
+
+				this.attributes.forEach(attr => {
+					addToSet(allDependencies, attr.dependencies);
+				});
+
 				this.attributes.forEach(attr => {
 					const { name, dependencies } = attr;
 
-					const condition = dependencies.size > 0
+					const condition = dependencies.size > 0 && (dependencies.size !== allDependencies.size)
 						? [...dependencies].map(d => `changed.${d}`).join(' || ')
 						: null;
 
@@ -181,20 +188,22 @@ export default class Component extends Node {
 					}
 				});
 
-				block.addVariable(levels);
-
-				statements.push(deindent`
-					${levels} = [
+				block.builders.init.addBlock(deindent`
+					var ${levels} = [
 						${initialProps.join(',\n')}
 					];
+				`);
 
+				statements.push(deindent`
 					for (var #i = 0; #i < ${levels}.length; #i += 1) {
 						${name_initial_data} = @assign(${name_initial_data}, ${levels}[#i]);
 					}
 				`);
 
+				const conditions = [...allDependencies].map(dep => `changed.${dep}`).join(' || ');
+
 				updates.push(deindent`
-					var ${name_changes} = @getSpreadUpdate(${levels}, [
+					var ${name_changes} = ${allDependencies.size === 1 ? `${conditions}` : `(${conditions})`} && @getSpreadUpdate(${levels}, [
 						${changes.join(',\n')}
 					]);
 				`);
@@ -381,6 +390,12 @@ export default class Component extends Node {
 
 			const updateMountNode = this.getUpdateMountNode(anchor);
 
+			if (updates.length) {
+				block.builders.update.addBlock(deindent`
+					${updates}
+				`);
+			}
+
 			block.builders.update.addBlock(deindent`
 				if (${switch_value} !== (${switch_value} = ${snippet})) {
 					if (${name}) ${name}.destroy();
@@ -408,8 +423,7 @@ export default class Component extends Node {
 
 			if (updates.length) {
 				block.builders.update.addBlock(deindent`
-					else {
-						${updates}
+					else if (${switch_value}) {
 						${name}._set(${name_changes});
 						${this.bindings.length && `${name_updating} = {};`}
 					}


Test patch:
diff --git a/test/runtime/samples/spread-component-dynamic-undefined/_config.js b/test/runtime/samples/spread-component-dynamic-undefined/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/spread-component-dynamic-undefined/_config.js
@@ -0,0 +1,19 @@
+export default {
+	data: {
+		props: {
+			a: 1,
+		},
+	},
+
+	html: ``,
+
+	test(assert, component, target) {
+		component.set({
+			props: {
+				a: 2,
+			},
+		});
+
+		assert.htmlEqual(target.innerHTML, ``);
+	},
+};
diff --git a/test/runtime/samples/spread-component-dynamic-undefined/main.html b/test/runtime/samples/spread-component-dynamic-undefined/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/spread-component-dynamic-undefined/main.html
@@ -0,0 +1 @@
+<svelte:component this={undefined} {...props} />
\ No newline at end of file