Repository: sveltejs/svelte (JavaScript)
Date: 2017-05-28 20:02:08+00:00
Task type: Bug Fix

Issue:
Compiler error "Cannot read property 'name' of null" with two-way-binding + brackets
Svelte version 1.20.2

Template snippet:

```html
<input type='checkbox' bind:checked='todo["done"]'>
```

Expected behavior: should compile as happily as if I had used `bind:checked='todo.done'`

Observed behavior: compiler errors out with `Cannot read property 'name' of null`

Reproduction: https://svelte.technology/repl?version=1.20.2&gist=6bc409d8cf45766230d012b355ebcf7c
Compiler error "Cannot read property 'name' of null" with two-way-binding + brackets
Svelte version 1.20.2

Template snippet:

```html
<input type='checkbox' bind:checked='todo["done"]'>
```

Expected behavior: should compile as happily as if I had used `bind:checked='todo.done'`

Observed behavior: compiler errors out with `Cannot read property 'name' of null`

Reproduction: https://svelte.technology/repl?version=1.20.2&gist=6bc409d8cf45766230d012b355ebcf7c


Issue discussion:

Solution patch:
diff --git a/src/generators/dom/visitors/Component/Binding.ts b/src/generators/dom/visitors/Component/Binding.ts
--- a/src/generators/dom/visitors/Component/Binding.ts
+++ b/src/generators/dom/visitors/Component/Binding.ts
@@ -39,7 +39,7 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
 		prop
 	});
 
-	const setter = getSetter({ block, name, context: '_context', attribute, dependencies, value: 'value' });
+	const setter = getSetter({ block, name, snippet, context: '_context', attribute, dependencies, value: 'value' });
 
 	generator.hasComplexBindings = true;
 
diff --git a/src/generators/dom/visitors/Element/Binding.ts b/src/generators/dom/visitors/Element/Binding.ts
--- a/src/generators/dom/visitors/Element/Binding.ts
+++ b/src/generators/dom/visitors/Element/Binding.ts
@@ -7,9 +7,16 @@ import Block from '../../Block';
 import { Node } from '../../../../interfaces';
 import { State } from '../../interfaces';
 
+function getObject ( node ) {
+	// TODO validation should ensure this is an Identifier or a MemberExpression
+	while ( node.type === 'MemberExpression' ) node = node.object;
+	return node;
+}
+
 export default function visitBinding ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node ) {
-	const { name, parts } = flattenReference( attribute.value );
-	const { snippet, contexts, dependencies } = block.contextualise( attribute.value );
+	const { name } = getObject( attribute.value );
+	const { snippet, contexts } = block.contextualise( attribute.value );
+	const dependencies = block.contextDependencies.has( name ) ? block.contextDependencies.get( name ) : [ name ];
 
 	if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' );
 
@@ -21,10 +28,10 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
 	const handler = block.getUniqueName( `${state.parentNode}_${eventName}_handler` );
 	const isMultipleSelect = node.name === 'select' && node.attributes.find( ( attr: Node ) => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue
 	const type = getStaticAttributeValue( node, 'type' );
-	const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, parts.join( '.' ) ) : null;
+	const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, attribute.value ) : null;
 	const value = getBindingValue( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type );
 
-	let setter = getSetter({ block, name, context: '_svelte', attribute, dependencies, value });
+	let setter = getSetter({ block, name, snippet, context: '_svelte', attribute, dependencies, value });
 	let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`;
 	const lock = block.alias( `${state.parentNode}_updating` );
 	let updateCondition = `!${lock}`;
@@ -190,7 +197,10 @@ function getBindingValue ( generator: DomGenerator, block: Block, state: State,
 	return `${state.parentNode}.${attribute.name}`;
 }
 
-function getBindingGroup ( generator: DomGenerator, keypath: string ) {
+function getBindingGroup ( generator: DomGenerator, value: Node ) {
+	const { parts } = flattenReference( value ); // TODO handle cases involving computed member expressions
+	const keypath = parts.join( '.' );
+
 	// TODO handle contextual bindings — `keypath` should include unique ID of
 	// each block that provides context
 	let index = generator.bindingGroups.indexOf( keypath );
diff --git a/src/generators/dom/visitors/shared/binding/getSetter.ts b/src/generators/dom/visitors/shared/binding/getSetter.ts
--- a/src/generators/dom/visitors/shared/binding/getSetter.ts
+++ b/src/generators/dom/visitors/shared/binding/getSetter.ts
@@ -1,14 +1,16 @@
 import deindent from '../../../../../utils/deindent.js';
 
-export default function getSetter ({ block, name, context, attribute, dependencies, value }) {
+export default function getSetter ({ block, name, snippet, context, attribute, dependencies, value }) {
 	const tail = attribute.value.type === 'MemberExpression' ? getTailSnippet( attribute.value ) : '';
 
 	if ( block.contexts.has( name ) ) {
 		const prop = dependencies[0];
+		const computed = isComputed( attribute.value );
 
 		return deindent`
 			var list = this.${context}.${block.listNames.get( name )};
 			var index = this.${context}.${block.indexNames.get( name )};
+			${computed && `var state = ${block.component}.get();`}
 			list[index]${tail} = ${value};
 
 			${block.component}._set({ ${prop}: ${block.component}.get( '${prop}' ) });
@@ -19,9 +21,9 @@ export default function getSetter ({ block, name, context, attribute, dependenci
 		const alias = block.alias( name );
 
 		return deindent`
-			var ${alias} = ${block.component}.get( '${name}' );
-			${alias}${tail} = ${value};
-			${block.component}._set({ ${name}: ${alias} });
+			var state = ${block.component}.get();
+			${snippet} = ${value};
+			${block.component}._set({ ${name}: state.${name} });
 		`;
 	}
 
@@ -35,3 +37,12 @@ function getTailSnippet ( node ) {
 
 	return `[✂${start}-${end}✂]`;
 }
+
+function isComputed ( node ) {
+	while ( node.type === 'MemberExpression' ) {
+		if ( node.computed ) return true;
+		node = node.object;
+	}
+
+	return false;
+}
\ No newline at end of file
diff --git a/src/parse/read/directives.ts b/src/parse/read/directives.ts
--- a/src/parse/read/directives.ts
+++ b/src/parse/read/directives.ts
@@ -104,7 +104,7 @@ export function readBindingDirective ( parser: Parser, start: number, name: stri
 		value = parseExpressionAt( source, a );
 
 		if ( value.type !== 'Identifier' && value.type !== 'MemberExpression' ) {
-			parser.error( `Expected valid property name` );
+			parser.error( `Cannot bind to rvalue`, value.start );
 		}
 
 		parser.allowWhitespace();


Test patch:
diff --git a/test/parser/samples/error-binding-rvalue/error.json b/test/parser/samples/error-binding-rvalue/error.json
new file mode 100644
--- /dev/null
+++ b/test/parser/samples/error-binding-rvalue/error.json
@@ -0,0 +1,8 @@
+{
+	"message": "Cannot bind to rvalue",
+	"pos": 19,
+	"loc": {
+		"line": 1,
+		"column": 19
+	}
+}
\ No newline at end of file
diff --git a/test/parser/samples/error-binding-rvalue/input.html b/test/parser/samples/error-binding-rvalue/input.html
new file mode 100644
--- /dev/null
+++ b/test/parser/samples/error-binding-rvalue/input.html
@@ -0,0 +1 @@
+<input bind:value='a + b'>
\ No newline at end of file
diff --git a/test/runtime/samples/binding-input-text-deep-computed-dynamic/_config.js b/test/runtime/samples/binding-input-text-deep-computed-dynamic/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-text-deep-computed-dynamic/_config.js
@@ -0,0 +1,55 @@
+export default {
+	data: {
+		prop: 'bar',
+		obj: {
+			foo: 'a',
+			bar: 'b',
+			baz: 'c'
+		}
+	},
+
+	html: `
+		<input>
+		<pre>{"foo":"a","bar":"b","baz":"c"}</pre>
+	`,
+
+	test ( assert, component, target, window ) {
+		const input = target.querySelector( 'input' );
+		const event = new window.Event( 'input' );
+
+		assert.equal( input.value, 'b' );
+
+		// edit bar
+		input.value = 'e';
+		input.dispatchEvent( event );
+
+		assert.htmlEqual( target.innerHTML, `
+			<input>
+			<pre>{"foo":"a","bar":"e","baz":"c"}</pre>
+		` );
+
+		// edit baz
+		component.set({ prop: 'baz' });
+		assert.equal( input.value, 'c' );
+
+		input.value = 'f';
+		input.dispatchEvent( event );
+
+		assert.htmlEqual( target.innerHTML, `
+			<input>
+			<pre>{"foo":"a","bar":"e","baz":"f"}</pre>
+		` );
+
+		// edit foo
+		component.set({ prop: 'foo' });
+		assert.equal( input.value, 'a' );
+
+		input.value = 'd';
+		input.dispatchEvent( event );
+
+		assert.htmlEqual( target.innerHTML, `
+			<input>
+			<pre>{"foo":"d","bar":"e","baz":"f"}</pre>
+		` );
+	}
+};
diff --git a/test/runtime/samples/binding-input-text-deep-computed-dynamic/main.html b/test/runtime/samples/binding-input-text-deep-computed-dynamic/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-text-deep-computed-dynamic/main.html
@@ -0,0 +1,2 @@
+<input bind:value='obj[prop]'>
+<pre>{{JSON.stringify(obj)}}</pre>
\ No newline at end of file
diff --git a/test/runtime/samples/binding-input-text-deep-computed/_config.js b/test/runtime/samples/binding-input-text-deep-computed/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-text-deep-computed/_config.js
@@ -0,0 +1,30 @@
+export default {
+	data: {
+		prop: 'name',
+		user: {
+			name: 'alice'
+		}
+	},
+
+	html: `<input>\n<p>hello alice</p>`,
+
+	test ( assert, component, target, window ) {
+		const input = target.querySelector( 'input' );
+
+		assert.equal( input.value, 'alice' );
+
+		const event = new window.Event( 'input' );
+
+		input.value = 'bob';
+		input.dispatchEvent( event );
+
+		assert.equal( target.innerHTML, `<input>\n<p>hello bob</p>` );
+
+		const user = component.get( 'user' );
+		user.name = 'carol';
+
+		component.set({ user });
+		assert.equal( input.value, 'carol' );
+		assert.equal( target.innerHTML, `<input>\n<p>hello carol</p>` );
+	}
+};
diff --git a/test/runtime/samples/binding-input-text-deep-computed/main.html b/test/runtime/samples/binding-input-text-deep-computed/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-text-deep-computed/main.html
@@ -0,0 +1,2 @@
+<input bind:value='user[prop]'>
+<p>hello {{user.name}}</p>
diff --git a/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/_config.js b/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/_config.js
@@ -0,0 +1,55 @@
+export default {
+	data: {
+		prop: 'bar',
+		objects: [{
+			foo: 'a',
+			bar: 'b',
+			baz: 'c'
+		}]
+	},
+
+	html: `
+		<input>
+		<pre>{"foo":"a","bar":"b","baz":"c"}</pre>
+	`,
+
+	test ( assert, component, target, window ) {
+		const input = target.querySelector( 'input' );
+		const event = new window.Event( 'input' );
+
+		assert.equal( input.value, 'b' );
+
+		// edit bar
+		input.value = 'e';
+		input.dispatchEvent( event );
+
+		assert.htmlEqual( target.innerHTML, `
+			<input>
+			<pre>{"foo":"a","bar":"e","baz":"c"}</pre>
+		` );
+
+		// edit baz
+		component.set({ prop: 'baz' });
+		assert.equal( input.value, 'c' );
+
+		input.value = 'f';
+		input.dispatchEvent( event );
+
+		assert.htmlEqual( target.innerHTML, `
+			<input>
+			<pre>{"foo":"a","bar":"e","baz":"f"}</pre>
+		` );
+
+		// edit foo
+		component.set({ prop: 'foo' });
+		assert.equal( input.value, 'a' );
+
+		input.value = 'd';
+		input.dispatchEvent( event );
+
+		assert.htmlEqual( target.innerHTML, `
+			<input>
+			<pre>{"foo":"d","bar":"e","baz":"f"}</pre>
+		` );
+	}
+};
diff --git a/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/main.html b/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/main.html
@@ -0,0 +1,4 @@
+{{#each objects as obj}}
+	<input bind:value='obj[prop]'>
+	<pre>{{JSON.stringify(obj)}}</pre>
+{{/each}}
\ No newline at end of file