Skip to content

Commit 3aa1814

Browse files
authored
Merge pull request #386 from sveltejs/gh-312
bind:group for checkbox inputs
2 parents 5c436ac + 7b057e4 commit 3aa1814

File tree

11 files changed

+186
-21
lines changed

11 files changed

+186
-21
lines changed

src/generators/Generator.js

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export default class Generator {
2222
this.components = {};
2323
this.events = {};
2424

25+
this.bindingGroups = [];
26+
2527
// track which properties are needed, so we can provide useful info
2628
// in dev mode
2729
this.expectedProperties = {};

src/generators/dom/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,10 @@ export default function dom ( parsed, source, options, names ) {
343343
);
344344
}
345345

346+
if ( generator.bindingGroups.length ) {
347+
constructorBlock.addLine( `this._bindingGroups = [ ${Array( generator.bindingGroups.length ).fill( '[]' ).join( ', ' )} ];` );
348+
}
349+
346350
constructorBlock.addBlock( deindent`
347351
this._observers = {
348352
pre: Object.create( null ),

src/generators/dom/visitors/Element.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export default {
2020
allUsedContexts: [],
2121

2222
init: new CodeBuilder(),
23-
update: new CodeBuilder()
23+
update: new CodeBuilder(),
24+
teardown: new CodeBuilder()
2425
};
2526

2627
const isToplevel = generator.current.localElementDepth === 0;
@@ -85,6 +86,7 @@ export default {
8586

8687
generator.current.builders.init.addBlock( local.init );
8788
if ( !local.update.isEmpty() ) generator.current.builders.update.addBlock( local.update );
89+
if ( !local.teardown.isEmpty() ) generator.current.builders.teardown.addBlock( local.teardown );
8890

8991
generator.createMountStatement( name );
9092

src/generators/dom/visitors/attributes/addElementAttributes.js

+11-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import attributeLookup from './lookup.js';
22
import addElementBinding from './addElementBinding';
33
import deindent from '../../../../utils/deindent.js';
44
import flattenReference from '../../../../utils/flattenReference.js';
5+
import getStaticAttributeValue from './binding/getStaticAttributeValue.js';
56

67
export default function addElementAttributes ( generator, node, local ) {
78
node.attributes.forEach( attribute => {
@@ -13,8 +14,12 @@ export default function addElementAttributes ( generator, node, local ) {
1314

1415
let dynamic = false;
1516

16-
const isBoundOptionValue = node.name === 'option' && name === 'value'; // TODO check it's actually bound
17-
const propertyName = isBoundOptionValue ? '__value' : metadata && metadata.propertyName;
17+
const isIndirectlyBoundValue = name === 'value' && (
18+
node.name === 'option' || // TODO check it's actually bound
19+
node.name === 'input' && /^(checkbox|radio)$/.test( getStaticAttributeValue( node, 'type' ) )
20+
);
21+
22+
const propertyName = isIndirectlyBoundValue ? '__value' : metadata && metadata.propertyName;
1823

1924
const isXlink = name.slice( 0, 6 ) === 'xlink:';
2025

@@ -134,12 +139,11 @@ export default function addElementAttributes ( generator, node, local ) {
134139
local.update.addLine( updater );
135140
}
136141

137-
if ( isBoundOptionValue ) {
138-
local.init.addLine( `${local.name}.value = ${local.name}.__value` );
142+
if ( isIndirectlyBoundValue ) {
143+
const updateValue = `${local.name}.value = ${local.name}.__value;`;
139144

140-
if (dynamic) {
141-
local.update.addLine( `${local.name}.value = ${local.name}.__value` );
142-
}
145+
local.init.addLine( updateValue );
146+
if ( dynamic ) local.update.addLine( updateValue );
143147
}
144148
}
145149

src/generators/dom/visitors/attributes/addElementBinding.js

+60-11
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import deindent from '../../../../utils/deindent.js';
22
import flattenReference from '../../../../utils/flattenReference.js';
33
import getSetter from './binding/getSetter.js';
4+
import getStaticAttributeValue from './binding/getStaticAttributeValue.js';
45

56
export default function createBinding ( generator, node, attribute, current, local ) {
6-
const { name } = flattenReference( attribute.value );
7+
const { name, keypath } = flattenReference( attribute.value );
78
const { snippet, contexts, dependencies } = generator.contextualise( attribute.value );
89

910
if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' );
@@ -14,20 +15,20 @@ export default function createBinding ( generator, node, attribute, current, loc
1415

1516
const handler = current.getUniqueName( `${local.name}ChangeHandler` );
1617

17-
const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO ensure that this is a static attribute
18-
const value = getBindingValue( local, node, attribute, isMultipleSelect );
18+
const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue
19+
const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, current, attribute, keypath ) : null;
20+
const value = getBindingValue( generator, local, node, attribute, isMultipleSelect, bindingGroup );
1921
const eventName = getBindingEventName( node );
2022

2123
let setter = getSetter({ current, name, context: '__svelte', attribute, dependencies, snippet, value });
22-
23-
// special case
24-
if ( node.name === 'select' && !isMultipleSelect ) {
25-
setter = `var selectedOption = ${local.name}.selectedOptions[0] || ${local.name}.options[0];\n` + setter;
26-
}
27-
2824
let updateElement;
2925

26+
// <select> special case
3027
if ( node.name === 'select' ) {
28+
if ( !isMultipleSelect ) {
29+
setter = `var selectedOption = ${local.name}.selectedOptions[0] || ${local.name}.options[0];\n` + setter;
30+
}
31+
3132
const value = generator.current.getUniqueName( 'value' );
3233
const i = generator.current.getUniqueName( 'i' );
3334
const option = generator.current.getUniqueName( 'option' );
@@ -49,7 +50,35 @@ export default function createBinding ( generator, node, attribute, current, loc
4950
${ifStatement}
5051
}
5152
`;
52-
} else {
53+
}
54+
55+
// <input type='checkbox|radio' bind:group='selected'> special case
56+
else if ( attribute.name === 'group' ) {
57+
const type = getStaticAttributeValue( node, 'type' );
58+
59+
if ( type === 'checkbox' ) {
60+
local.init.addLine(
61+
`component._bindingGroups[${bindingGroup}].push( ${local.name} );`
62+
);
63+
64+
local.teardown.addBlock(
65+
`component._bindingGroups[${bindingGroup}].splice( component._bindingGroups[${bindingGroup}].indexOf( ${local.name} ), 1 );`
66+
);
67+
68+
updateElement = `${local.name}.checked = ~${snippet}.indexOf( ${local.name}.__value );`;
69+
}
70+
71+
else if ( type === 'radio' ) {
72+
throw new Error( 'TODO' );
73+
}
74+
75+
else {
76+
throw new Error( `Unexpected bind:group` ); // TODO catch this in validation with a better error
77+
}
78+
}
79+
80+
// everything else
81+
else {
5382
updateElement = `${local.name}.${attribute.name} = ${snippet};`;
5483
}
5584

@@ -93,14 +122,34 @@ function getBindingEventName ( node ) {
93122
return 'change';
94123
}
95124

96-
function getBindingValue ( local, node, attribute, isMultipleSelect ) {
125+
function getBindingValue ( generator, local, node, attribute, isMultipleSelect, bindingGroup ) {
126+
// <select multiple bind:value='selected>
97127
if ( isMultipleSelect ) {
98128
return `[].map.call( ${local.name}.selectedOptions, function ( option ) { return option.__value; })`;
99129
}
100130

131+
// <select bind:value='selected>
101132
if ( node.name === 'select' ) {
102133
return 'selectedOption && selectedOption.__value';
103134
}
104135

136+
// <input type='checkbox' bind:group='foo'>
137+
if ( attribute.name === 'group' ) {
138+
return `${generator.helper( 'getBindingGroupValue' )}( component._bindingGroups[${bindingGroup}] )`;
139+
}
140+
141+
// everything else
105142
return `${local.name}.${attribute.name}`;
143+
}
144+
145+
function getBindingGroup ( generator, current, attribute, keypath ) {
146+
// TODO handle contextual bindings — `keypath` should include unique ID of
147+
// each block that provides context
148+
let index = generator.bindingGroups.indexOf( keypath );
149+
if ( index === -1 ) {
150+
index = generator.bindingGroups.length;
151+
generator.bindingGroups.push( keypath );
152+
}
153+
154+
return index;
106155
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default function getStaticAttributeValue ( node, name ) {
2+
const attribute = node.attributes.find( attr => attr.name.toLowerCase() === name );
3+
if ( !attribute ) return null;
4+
5+
if ( attribute.value.length !== 1 || attribute.value[0].type !== 'Text' ) {
6+
// TODO catch this in validation phase, give a more useful error (with location etc)
7+
throw new Error( `'${name} must be a static attribute` );
8+
}
9+
10+
return attribute.value[0].data;
11+
}

src/generators/dom/visitors/attributes/getStaticAttributeValue.js

Whitespace-only changes.

src/shared/dom.js

+8
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,11 @@ export function setAttribute ( node, attribute, value ) {
5353
export function setXlinkAttribute ( node, attribute, value ) {
5454
node.setAttributeNS( 'http://www.w3.org/1999/xlink', attribute, value );
5555
}
56+
57+
export function getBindingGroupValue ( group ) {
58+
var value = [];
59+
for ( var i = 0; i < group.length; i += 1 ) {
60+
if ( group[i].checked ) value.push( group[i].__value );
61+
}
62+
return value;
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const values = [
2+
{ name: 'Alpha' },
3+
{ name: 'Beta' },
4+
{ name: 'Gamma' }
5+
];
6+
7+
export default {
8+
data: {
9+
values,
10+
selected: [ values[1] ]
11+
},
12+
13+
'skip-ssr': true, // values are rendered as [object Object]
14+
15+
html: `
16+
<label>
17+
<input type="checkbox"> Alpha
18+
</label>
19+
20+
<label>
21+
<input type="checkbox"> Beta
22+
</label>
23+
24+
<label>
25+
<input type="checkbox"> Gamma
26+
</label>
27+
28+
<p>Beta</p>`,
29+
30+
test ( assert, component, target, window ) {
31+
const inputs = target.querySelectorAll( 'input' );
32+
assert.equal( inputs[0].checked, false );
33+
assert.equal( inputs[1].checked, true );
34+
assert.equal( inputs[2].checked, false );
35+
36+
const event = new window.Event( 'change' );
37+
38+
inputs[0].checked = true;
39+
inputs[0].dispatchEvent( event );
40+
41+
assert.htmlEqual( target.innerHTML, `
42+
<label>
43+
<input type="checkbox"> Alpha
44+
</label>
45+
46+
<label>
47+
<input type="checkbox"> Beta
48+
</label>
49+
50+
<label>
51+
<input type="checkbox"> Gamma
52+
</label>
53+
54+
<p>Alpha, Beta</p>
55+
` );
56+
57+
component.set({ selected: [ values[1], values[2] ] });
58+
assert.equal( inputs[0].checked, false );
59+
assert.equal( inputs[1].checked, true );
60+
assert.equal( inputs[2].checked, true );
61+
62+
assert.htmlEqual( target.innerHTML, `
63+
<label>
64+
<input type="checkbox"> Alpha
65+
</label>
66+
67+
<label>
68+
<input type="checkbox"> Beta
69+
</label>
70+
71+
<label>
72+
<input type="checkbox"> Gamma
73+
</label>
74+
75+
<p>Beta, Gamma</p>
76+
` );
77+
}
78+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{{#each values as value}}
2+
<label>
3+
<input type="checkbox" value="{{value}}" bind:group='selected' /> {{value.name}}
4+
</label>
5+
{{/each}}
6+
7+
<p>{{selected.map( function ( value ) { return value.name; }).join( ', ' ) }}</p>

test/helpers.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ function cleanChildren ( node ) {
102102
export function setupHtmlEqual () {
103103
return env().then( window => {
104104
assert.htmlEqual = ( actual, expected, message ) => {
105-
window.document.body.innerHTML = actual.trim();
105+
window.document.body.innerHTML = actual.replace( />[\s\r\n]+</g, '><' ).trim();
106106
cleanChildren( window.document.body, '' );
107107
actual = window.document.body.innerHTML;
108108

109-
window.document.body.innerHTML = expected.trim();
109+
window.document.body.innerHTML = expected.replace( />[\s\r\n]+</g, '><' ).trim();
110110
cleanChildren( window.document.body, '' );
111111
expected = window.document.body.innerHTML;
112112

0 commit comments

Comments
 (0)