Repository: sveltejs/svelte (JavaScript)
Date: 2019-01-19 14:37:10+00:00
Task type: Bug Fix

Issue:
component.set({ foo: undefined }) should 'unset' foo
In the following case...

```html
<svelte:component this={Whatever} bind:foo/>
```

...it's very likely that when `Whenever` changes, you don't want the new component to have the value of `foo` from the old component imposed upon it. But if you try to clear the value...

```js
this.set({ foo: undefined });
this.set({ Whatever: SomeNewComponent });
```

...then the `SomeNewComponent` instance is initialised with `foo: undefined` rather than being able to use its default value, which is what you want in that situation. The workaround is somewhat hacky:

```js
delete this._state.foo;
this.set({ Whatever: SomeNewComponent });
```

I'd argue that the current behaviour is a bug, and that `undefined` values should be deleted from the state object automatically. Outside of the `<svelte:component>` case, I don't think it would result in any different outcomes.


Issue discussion:
We *might* not need this, because of #1490 
Would it make sense to have a `this.del('key')` that explicitly removes a key from state? (Though in all honestly, I can't think of when I've actually needed this.)
In Svelte 3 we seem to have the opposite bug — the child value always overrides the parent value https://v3.svelte.technology/repl?version=3.0.0-alpha12&gist=181691a0863f4a40fd80f05ba81736b5

Solution patch:
diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts
--- a/src/compile/render-dom/index.ts
+++ b/src/compile/render-dom/index.ts
@@ -275,7 +275,7 @@ export default function dom(
 
 	const definition = has_definition
 		? component.alias('instance')
-		: '@identity';
+		: 'null';
 
 	const all_reactive_dependencies = new Set();
 	component.reactive_declarations.forEach(d => {
diff --git a/src/internal/Component.js b/src/internal/Component.js
--- a/src/internal/Component.js
+++ b/src/internal/Component.js
@@ -59,7 +59,7 @@ export function init(component, options, instance, create_fragment, not_equal) {
 
 	const $$ = component.$$ = {
 		fragment: null,
-		ctx: null,
+		ctx: options.props || {},
 
 		// state
 		set: noop,
@@ -82,19 +82,21 @@ export function init(component, options, instance, create_fragment, not_equal) {
 
 	let ready = false;
 
-	$$.ctx = instance(component, options.props || {}, (key, value) => {
-		if ($$.bound[key]) $$.bound[key](value);
+	if (instance) {
+		$$.ctx = instance(component, $$.ctx, (key, value) => {
+			if ($$.bound[key]) $$.bound[key](value);
 
-		if ($$.ctx) {
-			const changed = not_equal(value, $$.ctx[key]);
-			if (ready && changed) {
-				make_dirty(component, key);
-			}
+			if ($$.ctx) {
+				const changed = not_equal(value, $$.ctx[key]);
+				if (ready && changed) {
+					make_dirty(component, key);
+				}
 
-			$$.ctx[key] = value;
-			return changed;
-		}
-	});
+				$$.ctx[key] = value;
+				return changed;
+			}
+		});
+	}
 
 	$$.update();
 	ready = true;


Test patch:
diff --git a/test/js/samples/action/expected.js b/test/js/samples/action/expected.js
--- a/test/js/samples/action/expected.js
+++ b/test/js/samples/action/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, createElement, detachNode, identity, init, insert, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, createElement, detachNode, init, insert, noop, safe_not_equal } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	var a, link_action;
@@ -48,7 +48,7 @@ function link(node) {
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/js/samples/css-media-query/expected.js b/test/js/samples/css-media-query/expected.js
--- a/test/js/samples/css-media-query/expected.js
+++ b/test/js/samples/css-media-query/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, append, createElement, detachNode, identity, init, insert, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, append, createElement, detachNode, init, insert, noop, safe_not_equal } from "svelte/internal";
 
 function add_css() {
 	var style = createElement("style");
@@ -37,7 +37,7 @@ class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
 		if (!document.getElementById("svelte-1slhpfn-style")) add_css();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
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
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteElement, createElement, detachNode, identity, init, insert, noop, safe_not_equal } from "svelte/internal";
+import { SvelteElement, createElement, detachNode, init, insert, noop, safe_not_equal } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	var div;
@@ -33,7 +33,7 @@ class SvelteComponent extends SvelteElement {
 
 		this.shadowRoot.innerHTML = `<style>div{animation:foo 1s}@keyframes foo{0%{opacity:0}100%{opacity:1}}</style>`;
 
-		init(this, { target: this.shadowRoot }, identity, create_fragment, safe_not_equal);
+		init(this, { target: this.shadowRoot }, null, create_fragment, safe_not_equal);
 
 		if (options) {
 			if (options.target) {
diff --git a/test/js/samples/dynamic-import/expected.js b/test/js/samples/dynamic-import/expected.js
--- a/test/js/samples/dynamic-import/expected.js
+++ b/test/js/samples/dynamic-import/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, identity, init, mount_component, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, init, mount_component, noop, safe_not_equal } from "svelte/internal";
 import LazyLoad from "./LazyLoad.html";
 
 function create_fragment($$, ctx) {
@@ -43,7 +43,7 @@ function func() {
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/js/samples/event-handler-no-passive/expected.js b/test/js/samples/event-handler-no-passive/expected.js
--- a/test/js/samples/event-handler-no-passive/expected.js
+++ b/test/js/samples/event-handler-no-passive/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, addListener, createElement, detachNode, identity, init, insert, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, addListener, createElement, detachNode, init, insert, noop, safe_not_equal } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	var a, dispose;
@@ -37,7 +37,7 @@ function touchstart_handler(e) {
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/js/samples/event-modifiers/expected.js b/test/js/samples/event-modifiers/expected.js
--- a/test/js/samples/event-modifiers/expected.js
+++ b/test/js/samples/event-modifiers/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, addListener, append, createElement, createText, detachNode, identity, init, insert, noop, preventDefault, run_all, safe_not_equal, stopPropagation } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, addListener, append, createElement, createText, detachNode, init, insert, noop, preventDefault, run_all, safe_not_equal, stopPropagation } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	var div, button0, text1, button1, text3, button2, dispose;
@@ -57,7 +57,7 @@ function handleClick() {
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/js/samples/head-no-whitespace/expected.js b/test/js/samples/head-no-whitespace/expected.js
--- a/test/js/samples/head-no-whitespace/expected.js
+++ b/test/js/samples/head-no-whitespace/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, append, createElement, detachNode, identity, init, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, append, createElement, detachNode, init, noop, safe_not_equal } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	var meta0, meta1;
@@ -33,7 +33,7 @@ function create_fragment($$, ctx) {
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/js/samples/hoisted-const/expected.js b/test/js/samples/hoisted-const/expected.js
--- a/test/js/samples/hoisted-const/expected.js
+++ b/test/js/samples/hoisted-const/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, append, createElement, createText, detachNode, identity, init, insert, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, append, createElement, createText, detachNode, init, insert, noop, safe_not_equal } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	var b, text_value = get_answer(), text;
@@ -34,7 +34,7 @@ function get_answer() { return ANSWER; }
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/js/samples/hoisted-let/expected.js b/test/js/samples/hoisted-let/expected.js
--- a/test/js/samples/hoisted-let/expected.js
+++ b/test/js/samples/hoisted-let/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, append, createElement, createText, detachNode, identity, init, insert, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, append, createElement, createText, detachNode, init, insert, noop, safe_not_equal } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	var b, text_value = get_answer(), text;
@@ -34,7 +34,7 @@ function get_answer() { return ANSWER; }
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/js/samples/legacy-input-type/expected.js b/test/js/samples/legacy-input-type/expected.js
--- a/test/js/samples/legacy-input-type/expected.js
+++ b/test/js/samples/legacy-input-type/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, createElement, detachNode, identity, init, insert, noop, safe_not_equal, setInputType } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, createElement, detachNode, init, insert, noop, safe_not_equal, setInputType } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	var input;
@@ -29,7 +29,7 @@ function create_fragment($$, ctx) {
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/js/samples/non-imported-component/expected.js b/test/js/samples/non-imported-component/expected.js
--- a/test/js/samples/non-imported-component/expected.js
+++ b/test/js/samples/non-imported-component/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, createText, detachNode, identity, init, insert, mount_component, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, createText, detachNode, init, insert, mount_component, noop, safe_not_equal } from "svelte/internal";
 import Imported from "Imported.html";
 
 function create_fragment($$, ctx) {
@@ -54,7 +54,7 @@ function create_fragment($$, ctx) {
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/js/samples/non-mutable-reference/expected.js b/test/js/samples/non-mutable-reference/expected.js
--- a/test/js/samples/non-mutable-reference/expected.js
+++ b/test/js/samples/non-mutable-reference/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, append, createElement, createText, detachNode, identity, init, insert, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, append, createElement, createText, detachNode, init, insert, noop, safe_not_equal } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	var h1, text0, text1, text2;
@@ -36,7 +36,7 @@ let name = 'world';
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/js/samples/setup-method/expected.js b/test/js/samples/setup-method/expected.js
--- a/test/js/samples/setup-method/expected.js
+++ b/test/js/samples/setup-method/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, identity, init, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, init, noop, safe_not_equal } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	return {
@@ -21,7 +21,7 @@ function foo(bar) {
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 
 	get foo() {
diff --git a/test/js/samples/svg-title/expected.js b/test/js/samples/svg-title/expected.js
--- a/test/js/samples/svg-title/expected.js
+++ b/test/js/samples/svg-title/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, append, createSvgElement, createText, detachNode, identity, init, insert, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, append, createSvgElement, createText, detachNode, init, insert, noop, safe_not_equal } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	var svg, title, text;
@@ -32,7 +32,7 @@ function create_fragment($$, ctx) {
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, identity, create_fragment, safe_not_equal);
+		init(this, options, null, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/runtime/samples/component-binding-parent-supercedes-child-b/Bar.html b/test/runtime/samples/component-binding-parent-supercedes-child-b/Bar.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-parent-supercedes-child-b/Bar.html
@@ -0,0 +1,5 @@
+<script>
+	export let x = 'no';
+</script>
+
+<p>Bar: {x}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/component-binding-parent-supercedes-child-b/Foo.html b/test/runtime/samples/component-binding-parent-supercedes-child-b/Foo.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-parent-supercedes-child-b/Foo.html
@@ -0,0 +1,5 @@
+<script>
+	export let x = 'yes';
+</script>
+
+<p>Foo: {x}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/component-binding-parent-supercedes-child-b/_config.js b/test/runtime/samples/component-binding-parent-supercedes-child-b/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-parent-supercedes-child-b/_config.js
@@ -0,0 +1,31 @@
+export default {
+	html: `
+		<p>Foo: yes</p>
+		<p>x in parent: yes</p>
+	`,
+
+	async test({ assert, component, target, window }) {
+		component.a = false;
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>Bar: yes</p>
+			<p>x in parent: yes</p>
+		`);
+
+		component.a = true;
+		assert.equal(component.x, 'yes');
+		component.x = undefined;
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>Foo: undefined</p>
+			<p>x in parent: undefined</p>
+		`);
+
+		component.a = false;
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>Bar: no</p>
+			<p>x in parent: no</p>
+		`);
+	}
+};
diff --git a/test/runtime/samples/component-binding-parent-supercedes-child-b/main.html b/test/runtime/samples/component-binding-parent-supercedes-child-b/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-parent-supercedes-child-b/main.html
@@ -0,0 +1,15 @@
+<script>
+	import Foo from './Foo.html';
+	import Bar from './Bar.html';
+
+	export let a = true;
+	export let x;
+</script>
+
+{#if a}
+	<Foo bind:x/>
+{:else}
+	<Bar bind:x/>
+{/if}
+
+<p>x in parent: {x}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/component-binding-parent-supercedes-child-c/Bar.html b/test/runtime/samples/component-binding-parent-supercedes-child-c/Bar.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-parent-supercedes-child-c/Bar.html
@@ -0,0 +1,5 @@
+<script>
+	export let x = 'no';
+</script>
+
+<p>Bar: {x}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/component-binding-parent-supercedes-child-c/Foo.html b/test/runtime/samples/component-binding-parent-supercedes-child-c/Foo.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-parent-supercedes-child-c/Foo.html
@@ -0,0 +1,5 @@
+<script>
+	export let x = 'yes';
+</script>
+
+<p>Foo: {x}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/component-binding-parent-supercedes-child-c/_config.js b/test/runtime/samples/component-binding-parent-supercedes-child-c/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-parent-supercedes-child-c/_config.js
@@ -0,0 +1,31 @@
+export default {
+	html: `
+		<p>Foo: yes</p>
+		<p>x in parent: yes</p>
+	`,
+
+	async test({ assert, component, target, window }) {
+		component.a = false;
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>Bar: yes</p>
+			<p>x in parent: yes</p>
+		`);
+
+		component.a = true;
+		assert.equal(component.x, 'yes');
+		component.x = undefined;
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>Foo: undefined</p>
+			<p>x in parent: undefined</p>
+		`);
+
+		component.a = false;
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>Bar: no</p>
+			<p>x in parent: no</p>
+		`);
+	}
+};
diff --git a/test/runtime/samples/component-binding-parent-supercedes-child-c/main.html b/test/runtime/samples/component-binding-parent-supercedes-child-c/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-parent-supercedes-child-c/main.html
@@ -0,0 +1,11 @@
+<script>
+	import Foo from './Foo.html';
+	import Bar from './Bar.html';
+
+	export let a = true;
+	export let x;
+</script>
+
+<svelte:component this="{a ? Foo : Bar}" bind:x/>
+
+<p>x in parent: {x}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/component-binding-private-state/Bar.html b/test/runtime/samples/component-binding-private-state/Bar.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-private-state/Bar.html
@@ -0,0 +1,5 @@
+<script>
+	let x = 'no';
+</script>
+
+<p>Bar: {x}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/component-binding-private-state/Foo.html b/test/runtime/samples/component-binding-private-state/Foo.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-private-state/Foo.html
@@ -0,0 +1,5 @@
+<script>
+	let x = 'yes';
+</script>
+
+<p>Foo: {x}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/component-binding-private-state/_config.js b/test/runtime/samples/component-binding-private-state/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-private-state/_config.js
@@ -0,0 +1,31 @@
+export default {
+	html: `
+		<p>Foo: yes</p>
+		<p>x in parent: undefined</p>
+	`,
+
+	async test({ assert, component, target, window }) {
+		component.a = false;
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>Bar: no</p>
+			<p>x in parent: undefined</p>
+		`);
+
+		component.a = true;
+		assert.equal(component.x, undefined);
+		component.x = 'maybe';
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>Foo: yes</p>
+			<p>x in parent: maybe</p>
+		`);
+
+		component.a = false;
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>Bar: no</p>
+			<p>x in parent: maybe</p>
+		`);
+	}
+};
diff --git a/test/runtime/samples/component-binding-private-state/main.html b/test/runtime/samples/component-binding-private-state/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-private-state/main.html
@@ -0,0 +1,11 @@
+<script>
+	import Foo from './Foo.html';
+	import Bar from './Bar.html';
+
+	export let a = true;
+	export let x;
+</script>
+
+<svelte:component this="{a ? Foo : Bar}" bind:x/>
+
+<p>x in parent: {x}</p>
\ No newline at end of file