Repository: sveltejs/svelte (JavaScript)
Date: 2017-05-19 02:21:12+00:00
Task type: Bug Fix

Issue:
#each+<select>+on:change are not working
You can see this bug here: https://svelte.technology/repl?gist=1c4a9db0b3ce319f4e893138e5a6398d

list0 - on:change is working without problems

lists inside #each block - on:change is not working (Uncaught TypeError: Cannot read property 'component' of undefined
    at HTMLSelectElement.change_handler (eval at U (bundle.61a4bf9….js:6), <anonymous>:170:30)
change_handler	@	VM816:170
)

inputs inside #each block - on:change is working without problems



Issue discussion:
This looks related to my changes in #521. `childState.allUsedContexts` and `childState.usesComponent` apparently aren't getting updated in time when we're deferring visiting all of the `<select>` element's attributes - and so we don't generate the code that initializes or updates the values in `_svelte` on the element.

Solution patch:
diff --git a/src/generators/dom/visitors/Element/Element.js b/src/generators/dom/visitors/Element/Element.js
--- a/src/generators/dom/visitors/Element/Element.js
+++ b/src/generators/dom/visitors/Element/Element.js
@@ -46,7 +46,7 @@ export default function visitElement ( generator, block, state, node ) {
 		block.builders.create.addLine( `${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );` );
 	}
 
-	function visitAttributes () {
+	function visitAttributesAndAddProps () {
 		let intro;
 		let outro;
 
@@ -63,6 +63,37 @@ export default function visitElement ( generator, block, state, node ) {
 			});
 
 		if ( intro || outro ) addTransitions( generator, block, childState, node, intro, outro );
+
+		if ( childState.allUsedContexts.length || childState.usesComponent ) {
+			const initialProps = [];
+			const updates = [];
+
+			if ( childState.usesComponent ) {
+				initialProps.push( `component: ${block.component}` );
+			}
+
+			childState.allUsedContexts.forEach( contextName => {
+				if ( contextName === 'state' ) return;
+
+				const listName = block.listNames.get( contextName );
+				const indexName = block.indexNames.get( contextName );
+
+				initialProps.push( `${listName}: ${listName},\n${indexName}: ${indexName}` );
+				updates.push( `${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};` );
+			});
+
+			if ( initialProps.length ) {
+				block.builders.create.addBlock( deindent`
+					${name}._svelte = {
+						${initialProps.join( ',\n' )}
+					};
+				` );
+			}
+
+			if ( updates.length ) {
+				block.builders.update.addBlock( updates.join( '\n' ) );
+			}
+		}
 	}
 
 	if ( !state.parentNode ) {
@@ -74,7 +105,7 @@ export default function visitElement ( generator, block, state, node ) {
 	if ( node.name !== 'select' ) {
 		// <select> value attributes are an annoying special case — it must be handled
 		// *after* its children have been updated
-		visitAttributes();
+		visitAttributesAndAddProps();
 	}
 
 	// special case – bound <option> without a value attribute
@@ -83,37 +114,6 @@ export default function visitElement ( generator, block, state, node ) {
 		node.initialUpdate = node.lateUpdate = statement;
 	}
 
-	if ( childState.allUsedContexts.length || childState.usesComponent ) {
-		const initialProps = [];
-		const updates = [];
-
-		if ( childState.usesComponent ) {
-			initialProps.push( `component: ${block.component}` );
-		}
-
-		childState.allUsedContexts.forEach( contextName => {
-			if ( contextName === 'state' ) return;
-
-			const listName = block.listNames.get( contextName );
-			const indexName = block.indexNames.get( contextName );
-
-			initialProps.push( `${listName}: ${listName},\n${indexName}: ${indexName}` );
-			updates.push( `${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};` );
-		});
-
-		if ( initialProps.length ) {
-			block.builders.create.addBlock( deindent`
-				${name}._svelte = {
-					${initialProps.join( ',\n' )}
-				};
-			` );
-		}
-
-		if ( updates.length ) {
-			block.builders.update.addBlock( updates.join( '\n' ) );
-		}
-	}
-
 	node.children.forEach( child => {
 		visit( generator, block, childState, child );
 	});
@@ -123,7 +123,7 @@ export default function visitElement ( generator, block, state, node ) {
 	}
 
 	if ( node.name === 'select' ) {
-		visitAttributes();
+		visitAttributesAndAddProps();
 	}
 
 	if ( node.initialUpdate ) {


Test patch:
diff --git a/test/runtime/samples/select-props/_config.js b/test/runtime/samples/select-props/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/select-props/_config.js
@@ -0,0 +1,15 @@
+export default {
+	test ( assert, component, target, window ) {
+		const selects = document.querySelectorAll( 'select' );
+
+		const event1 = new window.Event( 'change' );
+		selects[0].value = 'b';
+		selects[0].dispatchEvent(event1);
+
+		const event2 = new window.Event( 'change' );
+		selects[1].value = 'b';
+		selects[1].dispatchEvent(event2);
+
+		assert.deepEqual( component.get( 'log' ), [ 1, 2 ] );
+	}
+};
diff --git a/test/runtime/samples/select-props/main.html b/test/runtime/samples/select-props/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/select-props/main.html
@@ -0,0 +1,19 @@
+{{#each foo as bar}}
+	<select on:change='handler(bar)'>
+		<option>a</option>
+		<option>b</option>
+	</select>
+{{/each}}
+
+<script>
+	export default {
+		data: () => ({ foo: [ 1, 2 ], log: [] }),
+		methods: {
+			handler ( bar ) {
+				let { log } = this.get();
+				log.push( bar );
+				this.set( { log } );
+			}
+		}
+	}
+</script>