Skip to content

Commit 38de3b2

Browse files
authored
fix bind:group in each (#4868)
1 parent 9079416 commit 38de3b2

File tree

19 files changed

+1157
-42
lines changed

19 files changed

+1157
-42
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Svelte changelog
22

3+
## Unreleased
4+
5+
* Fix `bind:group` inside `{#each}` ([#3243](https://github.com/sveltejs/svelte/issues/3243))
6+
37
## 3.23.1
48

59
* Fix checkbox `bind:group` when multiple options have the same value ([#4397](https://github.com/sveltejs/svelte/issues/4397))

src/compiler/compile/nodes/EachBlock.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default class EachBlock extends AbstractBlock {
2121
contexts: Context[];
2222
has_animation: boolean;
2323
has_binding = false;
24+
has_index_binding = false;
2425

2526
else?: ElseBlock;
2627

src/compiler/compile/render_dom/Renderer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default class Renderer {
3232
blocks: Array<Block | Node | Node[]> = [];
3333
readonly: Set<string> = new Set();
3434
meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
35-
binding_groups: string[] = [];
35+
binding_groups: Map<string, { binding_group: (to_reference?: boolean) => Node; is_context: boolean; contexts: string[]; index: number }> = new Map();
3636

3737
block: Block;
3838
fragment: FragmentWrapper;
@@ -63,7 +63,7 @@ export default class Renderer {
6363
this.add_to_context('$$slots');
6464
}
6565

66-
if (this.binding_groups.length > 0) {
66+
if (this.binding_groups.size > 0) {
6767
this.add_to_context('$$binding_groups');
6868
}
6969

src/compiler/compile/render_dom/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ export default function dom(
416416
${component.slots.size || component.compile_options.dev ? b`let { $$slots = {}, $$scope } = $$props;` : null}
417417
${component.compile_options.dev && b`@validate_slots('${component.tag}', $$slots, [${[...component.slots.keys()].map(key => `'${key}'`).join(',')}]);`}
418418
419-
${renderer.binding_groups.length > 0 && b`const $$binding_groups = [${renderer.binding_groups.map(_ => x`[]`)}];`}
419+
${renderer.binding_groups.size > 0 && b`const $$binding_groups = [${[...renderer.binding_groups.keys()].map(_ => x`[]`)}];`}
420420
421421
${component.partly_hoisted}
422422

src/compiler/compile/render_dom/wrappers/EachBlock.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export default class EachBlockWrapper extends Wrapper {
201201
this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`list[i]`)};`);
202202

203203
if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
204-
if (this.node.has_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);
204+
if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);
205205

206206
const snippet = this.node.expression.manipulate(block);
207207

src/compiler/compile/render_dom/wrappers/Element/Binding.ts

+80-25
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import replace_object from '../../../utils/replace_object';
77
import Block from '../../Block';
88
import Renderer from '../../Renderer';
99
import flatten_reference from '../../../utils/flatten_reference';
10-
import EachBlock from '../../../nodes/EachBlock';
1110
import { Node, Identifier } from 'estree';
11+
import add_to_set from '../../../utils/add_to_set';
12+
import mark_each_block_bindings from '../shared/mark_each_block_bindings';
1213

1314
export default class BindingWrapper {
1415
node: Binding;
@@ -42,12 +43,7 @@ export default class BindingWrapper {
4243
}
4344

4445
if (node.is_contextual) {
45-
// we need to ensure that the each block creates a context including
46-
// the list and the index, if they're not otherwise referenced
47-
const { name } = get_object(this.node.expression.node);
48-
const each_block = this.parent.node.scope.get_owner(name);
49-
50-
(each_block as EachBlock).has_binding = true;
46+
mark_each_block_bindings(this.parent, this.node);
5147
}
5248

5349
this.object = get_object(this.node.expression.node).name;
@@ -123,17 +119,31 @@ export default class BindingWrapper {
123119
switch (this.node.name) {
124120
case 'group':
125121
{
126-
const binding_group = get_binding_group(parent.renderer, this.node.expression.node);
122+
const { binding_group, is_context, contexts, index } = get_binding_group(parent.renderer, this.node, block);
127123

128124
block.renderer.add_to_context(`$$binding_groups`);
129-
const reference = block.renderer.reference(`$$binding_groups`);
125+
126+
if (is_context) {
127+
if (contexts.length > 1) {
128+
let binding_group = x`${block.renderer.reference('$$binding_groups')}[${index}]`;
129+
for (const name of contexts.slice(0, -1)) {
130+
binding_group = x`${binding_group}[${block.renderer.reference(name)}]`;
131+
block.chunks.init.push(
132+
b`${binding_group} = ${binding_group} || [];`
133+
);
134+
}
135+
}
136+
block.chunks.init.push(
137+
b`${binding_group(true)} = [];`
138+
);
139+
}
130140

131141
block.chunks.hydrate.push(
132-
b`${reference}[${binding_group}].push(${parent.var});`
142+
b`${binding_group(true)}.push(${parent.var});`
133143
);
134144

135145
block.chunks.destroy.push(
136-
b`${reference}[${binding_group}].splice(${reference}[${binding_group}].indexOf(${parent.var}), 1);`
146+
b`${binding_group(true)}.splice(${binding_group(true)}.indexOf(${parent.var}), 1);`
137147
);
138148
break;
139149
}
@@ -245,19 +255,61 @@ function get_dom_updater(
245255
return b`${element.var}.${binding.node.name} = ${binding.snippet};`;
246256
}
247257

248-
function get_binding_group(renderer: Renderer, value: Node) {
249-
const { parts } = flatten_reference(value); // TODO handle cases involving computed member expressions
250-
const keypath = parts.join('.');
258+
function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
259+
const { parts } = flatten_reference(value.raw_expression);
260+
let keypath = parts.join('.');
261+
262+
const contexts = [];
263+
264+
for (const dep of value.expression.contextual_dependencies) {
265+
const context = block.bindings.get(dep);
266+
let key;
267+
let name;
268+
if (context) {
269+
key = context.object.name;
270+
name = context.property.name;
271+
} else {
272+
key = dep;
273+
name = dep;
274+
}
275+
keypath = `${key}@${keypath}`;
276+
contexts.push(name);
277+
}
278+
279+
if (!renderer.binding_groups.has(keypath)) {
280+
const index = renderer.binding_groups.size;
281+
282+
contexts.forEach(context => {
283+
renderer.add_to_context(context, true);
284+
});
251285

252-
// TODO handle contextual bindings — `keypath` should include unique ID of
253-
// each block that provides context
254-
let index = renderer.binding_groups.indexOf(keypath);
255-
if (index === -1) {
256-
index = renderer.binding_groups.length;
257-
renderer.binding_groups.push(keypath);
286+
renderer.binding_groups.set(keypath, {
287+
binding_group: (to_reference: boolean = false) => {
288+
let binding_group = '$$binding_groups';
289+
let _secondary_indexes = contexts;
290+
291+
if (to_reference) {
292+
binding_group = block.renderer.reference(binding_group);
293+
_secondary_indexes = _secondary_indexes.map(name => block.renderer.reference(name));
294+
}
295+
296+
if (_secondary_indexes.length > 0) {
297+
let obj = x`${binding_group}[${index}]`;
298+
_secondary_indexes.forEach(secondary_index => {
299+
obj = x`${obj}[${secondary_index}]`;
300+
});
301+
return obj;
302+
} else {
303+
return x`${binding_group}[${index}]`;
304+
}
305+
},
306+
is_context: contexts.length > 0,
307+
contexts,
308+
index,
309+
});
258310
}
259311

260-
return index;
312+
return renderer.binding_groups.get(keypath);
261313
}
262314

263315
function get_event_handler(
@@ -295,7 +347,7 @@ function get_event_handler(
295347
}
296348
}
297349

298-
const value = get_value_from_dom(renderer, binding.parent, binding);
350+
const value = get_value_from_dom(renderer, binding.parent, binding, block, contextual_dependencies);
299351

300352
const mutation = b`
301353
${lhs} = ${value};
@@ -313,7 +365,9 @@ function get_event_handler(
313365
function get_value_from_dom(
314366
renderer: Renderer,
315367
element: ElementWrapper | InlineComponentWrapper,
316-
binding: BindingWrapper
368+
binding: BindingWrapper,
369+
block: Block,
370+
contextual_dependencies: Set<string>
317371
) {
318372
const { node } = element;
319373
const { name } = binding.node;
@@ -333,9 +387,10 @@ function get_value_from_dom(
333387

334388
// <input type='checkbox' bind:group='foo'>
335389
if (name === 'group') {
336-
const binding_group = get_binding_group(renderer, binding.node.expression.node);
337390
if (type === 'checkbox') {
338-
return x`@get_binding_group_value($$binding_groups[${binding_group}], this.__value, this.checked)`;
391+
const { binding_group, contexts } = get_binding_group(renderer, binding.node, block);
392+
add_to_set(contextual_dependencies, contexts);
393+
return x`@get_binding_group_value(${binding_group()}, this.__value, this.checked)`;
339394
}
340395

341396
return x`this.__value`;

src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,15 @@ import { sanitize } from '../../../../utils/names';
88
import add_to_set from '../../../utils/add_to_set';
99
import { b, x, p } from 'code-red';
1010
import Attribute from '../../../nodes/Attribute';
11-
import get_object from '../../../utils/get_object';
1211
import create_debugging_comment from '../shared/create_debugging_comment';
1312
import { get_slot_definition } from '../shared/get_slot_definition';
14-
import EachBlock from '../../../nodes/EachBlock';
1513
import TemplateScope from '../../../nodes/shared/TemplateScope';
1614
import is_dynamic from '../shared/is_dynamic';
1715
import bind_this from '../shared/bind_this';
1816
import { Node, Identifier, ObjectExpression } from 'estree';
1917
import EventHandler from '../Element/EventHandler';
2018
import { extract_names } from 'periscopic';
19+
import mark_each_block_bindings from '../shared/mark_each_block_bindings';
2120

2221
export default class InlineComponentWrapper extends Wrapper {
2322
var: Identifier;
@@ -48,12 +47,7 @@ export default class InlineComponentWrapper extends Wrapper {
4847

4948
this.node.bindings.forEach(binding => {
5049
if (binding.is_contextual) {
51-
// we need to ensure that the each block creates a context including
52-
// the list and the index, if they're not otherwise referenced
53-
const { name } = get_object(binding.expression.node);
54-
const each_block = this.node.scope.get_owner(name);
55-
56-
(each_block as EachBlock).has_binding = true;
50+
mark_each_block_bindings(this, binding);
5751
}
5852

5953
block.add_dependencies(binding.expression.dependencies);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import EachBlock from "../../../nodes/EachBlock";
2+
import InlineComponentWrapper from "../InlineComponent";
3+
import ElementWrapper from "../Element";
4+
import Binding from "../../../nodes/Binding";
5+
import get_object from "../../../utils/get_object";
6+
7+
export default function mark_each_block_bindings(
8+
parent: ElementWrapper | InlineComponentWrapper,
9+
binding: Binding
10+
) {
11+
// we need to ensure that the each block creates a context including
12+
// the list and the index, if they're not otherwise referenced
13+
const object = get_object(binding.expression.node).name;
14+
const each_block = parent.node.scope.get_owner(object);
15+
(each_block as EachBlock).has_binding = true;
16+
17+
if (binding.name === "group") {
18+
// for `<input bind:group={} >`, we make sure that all the each blocks creates context with `index`
19+
for (const name of binding.expression.contextual_dependencies) {
20+
const each_block = parent.node.scope.get_owner(name);
21+
(each_block as EachBlock).has_index_binding = true;
22+
}
23+
}
24+
}

src/compiler/compile/utils/flatten_reference.ts

+16-5
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import { Node, Identifier } from 'estree';
33
export default function flatten_reference(node: Node) {
44
const nodes = [];
55
const parts = [];
6-
6+
77
while (node.type === 'MemberExpression') {
88
nodes.unshift(node.property);
99

1010
if (!node.computed) {
1111
parts.unshift((node.property as Identifier).name);
12+
} else {
13+
const computed_property = to_string(node.property);
14+
if (computed_property) {
15+
parts.unshift(`[${computed_property}]`);
16+
}
1217
}
13-
1418
node = node.object;
1519
}
1620

@@ -20,9 +24,16 @@ export default function flatten_reference(node: Node) {
2024

2125
nodes.unshift(node);
2226

23-
if (!(node as any).computed) {
24-
parts.unshift(name);
25-
}
27+
parts.unshift(name);
2628

2729
return { name, nodes, parts };
2830
}
31+
32+
function to_string(node: Node) {
33+
switch (node.type) {
34+
case 'Literal':
35+
return String(node.value);
36+
case 'Identifier':
37+
return node.name;
38+
}
39+
}

0 commit comments

Comments
 (0)