Repository: sveltejs/svelte (JavaScript)
Date: 2017-04-19 02:22:35+00:00
Task type: Bug Fix

Issue:
Grouped checkbox bindings behaving badly
[REPL](https://svelte.technology/repl?version=1.16.0&gist=067127646900e0e6e979e6ca64f9bbd5). Selecting a checkbox shouldn't deselect others in the group.


Issue discussion:

Solution patch:
diff --git a/src/generators/dom/visitors/EachBlock.js b/src/generators/dom/visitors/EachBlock.js
--- a/src/generators/dom/visitors/EachBlock.js
+++ b/src/generators/dom/visitors/EachBlock.js
@@ -57,16 +57,28 @@ export default function visitEachBlock ( generator, block, state, node ) {
 			}
 		` );
 
-		block.builders.update.addBlock( deindent`
-			if ( !${each_block_value}.length && ${each_block_else} ) {
-				${each_block_else}.update( changed, ${params} );
-			} else if ( !${each_block_value}.length ) {
-				${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
-				${each_block_else}.mount( ${anchor}.parentNode, ${anchor} );
-			} else if ( ${each_block_else} ) {
-				${each_block_else}.destroy( true );
-			}
-		` );
+		if ( node.else._block.hasUpdateMethod ) {
+			block.builders.update.addBlock( deindent`
+				if ( !${each_block_value}.length && ${each_block_else} ) {
+					${each_block_else}.update( changed, ${params} );
+				} else if ( !${each_block_value}.length ) {
+					${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
+					${each_block_else}.mount( ${anchor}.parentNode, ${anchor} );
+				} else if ( ${each_block_else} ) {
+					${each_block_else}.destroy( true );
+				}
+			` );
+		} else {
+			block.builders.update.addBlock( deindent`
+				if ( ${each_block_value}.length ) {
+					if ( ${each_block_else} ) ${each_block_else}.destroy( true );
+				} else if ( !${each_block_else} ) {
+					${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
+					${each_block_else}.mount( ${anchor}.parentNode, ${anchor} );
+				}
+			` );
+		}
+
 
 		block.builders.destroy.addBlock( deindent`
 			if ( ${each_block_else} ) {
diff --git a/src/generators/dom/visitors/Element/Binding.js b/src/generators/dom/visitors/Element/Binding.js
--- a/src/generators/dom/visitors/Element/Binding.js
+++ b/src/generators/dom/visitors/Element/Binding.js
@@ -4,7 +4,7 @@ import getSetter from '../shared/binding/getSetter.js';
 import getStaticAttributeValue from './getStaticAttributeValue.js';
 
 export default function visitBinding ( generator, block, state, node, attribute ) {
-	const { name, keypath } = flattenReference( attribute.value );
+	const { name, keypath, parts } = flattenReference( attribute.value );
 	const { snippet, contexts, dependencies } = block.contextualise( attribute.value );
 
 	if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' );
@@ -17,7 +17,7 @@ export default function visitBinding ( generator, block, state, node, attribute
 	const handler = block.getUniqueName( `${state.parentNode}_${eventName}_handler` );
 	const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue
 	const type = getStaticAttributeValue( node, 'type' );
-	const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, keypath ) : null;
+	const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, parts.join( '.' ) ) : null;
 	const value = getBindingValue( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type );
 
 	let setter = getSetter({ block, name, keypath, context: '_svelte', attribute, dependencies, value });


Test patch:
diff --git a/test/runtime/samples/binding-input-checkbox-group-outside-each/_config.js b/test/runtime/samples/binding-input-checkbox-group-outside-each/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-checkbox-group-outside-each/_config.js
@@ -0,0 +1,78 @@
+const values = [
+	{ name: 'Alpha' },
+	{ name: 'Beta' },
+	{ name: 'Gamma' }
+];
+
+export default {
+	data: {
+		values,
+		selected: [ values[1] ]
+	},
+
+	'skip-ssr': true, // values are rendered as [object Object]
+
+	html: `
+		<label>
+			<input type="checkbox"> Alpha
+		</label>
+
+		<label>
+			<input type="checkbox"> Beta
+		</label>
+
+		<label>
+			<input type="checkbox"> Gamma
+		</label>
+
+		<p>Beta</p>`,
+
+	test ( assert, component, target, window ) {
+		const inputs = target.querySelectorAll( 'input' );
+		assert.equal( inputs[0].checked, false );
+		assert.equal( inputs[1].checked, true );
+		assert.equal( inputs[2].checked, false );
+
+		const event = new window.Event( 'change' );
+
+		inputs[0].checked = true;
+		inputs[0].dispatchEvent( event );
+
+		assert.htmlEqual( target.innerHTML, `
+			<label>
+				<input type="checkbox"> Alpha
+			</label>
+
+			<label>
+				<input type="checkbox"> Beta
+			</label>
+
+			<label>
+				<input type="checkbox"> Gamma
+			</label>
+
+			<p>Alpha, Beta</p>
+		` );
+
+		component.set({ selected: [ values[1], values[2] ] });
+		assert.equal( inputs[0].checked, false );
+		assert.equal( inputs[1].checked, true );
+		assert.equal( inputs[2].checked, true );
+
+		assert.htmlEqual( target.innerHTML, `
+			<label>
+				<input type="checkbox"> Alpha
+			</label>
+
+			<label>
+				<input type="checkbox"> Beta
+			</label>
+
+			<label>
+				<input type="checkbox"> Gamma
+			</label>
+
+			<p>Beta, Gamma</p>
+		` );
+	}
+};
diff --git a/test/runtime/samples/binding-input-checkbox-group-outside-each/main.html b/test/runtime/samples/binding-input-checkbox-group-outside-each/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/binding-input-checkbox-group-outside-each/main.html
@@ -0,0 +1,13 @@
+<label>
+	<input type="checkbox" value="{{values[0]}}" bind:group='selected' /> {{values[0].name}}
+</label>
+
+<label>
+	<input type="checkbox" value="{{values[1]}}" bind:group='selected' /> {{values[1].name}}
+</label>
+
+<label>
+	<input type="checkbox" value="{{values[2]}}" bind:group='selected' /> {{values[2].name}}
+</label>
+
+<p>{{selected.map( function ( value ) { return value.name; }).join( ', ' ) }}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/each-block-dynamic-else-static/_config.js b/test/runtime/samples/each-block-dynamic-else-static/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-dynamic-else-static/_config.js
@@ -0,0 +1,27 @@
+export default {
+	data: {
+		animals: [ 'alpaca', 'baboon', 'capybara' ]
+	},
+
+	html: `
+		<p>alpaca</p>
+		<p>baboon</p>
+		<p>capybara</p>
+	`,
+
+	test ( assert, component, target ) {
+		component.set({ animals: [] });
+		assert.htmlEqual( target.innerHTML, `
+			<p>no animals</p>
+		` );
+
+		// trigger an 'update' of the else block, to ensure that
+		// non-existent update method is not called
+		component.set({ animals: [] });
+
+		component.set({ animals: ['wombat'] });
+		assert.htmlEqual( target.innerHTML, `
+			<p>wombat</p>
+		` );
+	}
+};
diff --git a/test/runtime/samples/each-block-dynamic-else-static/main.html b/test/runtime/samples/each-block-dynamic-else-static/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-dynamic-else-static/main.html
@@ -0,0 +1,5 @@
+{{#each animals as animal}}
+	<p>{{animal}}</p>
+{{else}}
+	<p>no animals</p>
+{{/each}}
\ No newline at end of file