Skip to content

Commit 0966d1d

Browse files
authored
feat: improve bind:group behavior (#7892)
track all `#each` variables that could result in a change to the inputs and also update the `$$binding_groups` variable which holds the references to the inputs of each group accordingly. Fixes #7633 Fixes #6112 Fixes #7884
1 parent 6476e9b commit 0966d1d

File tree

19 files changed

+791
-64
lines changed

19 files changed

+791
-64
lines changed

src/compiler/compile/render_dom/Block.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Renderer from './Renderer';
1+
import Renderer, { BindingGroup } from './Renderer';
22
import Wrapper from './wrappers/shared/Wrapper';
33
import { b, x } from 'code-red';
44
import { Node, Identifier, ArrayPattern } from 'estree';
@@ -40,6 +40,7 @@ export default class Block {
4040

4141
bindings: Map<string, Bindings>;
4242
binding_group_initialised: Set<string> = new Set();
43+
binding_groups: Set<BindingGroup> = new Set();
4344

4445
chunks: {
4546
declarations: Array<Node | Node[]>;
@@ -249,6 +250,7 @@ export default class Block {
249250
}
250251
}
251252

253+
this.render_binding_groups();
252254
this.render_listeners();
253255

254256
const properties: Record<string, any> = {};
@@ -500,4 +502,10 @@ export default class Block {
500502
}
501503
}
502504
}
505+
506+
render_binding_groups() {
507+
for (const binding_group of this.binding_groups) {
508+
binding_group.render();
509+
}
510+
}
503511
}

src/compiler/compile/render_dom/Renderer.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ type BitMasks = Array<{
2222
names: string[];
2323
}>;
2424

25+
export interface BindingGroup {
26+
binding_group: (to_reference?: boolean) => Node;
27+
contexts: string[];
28+
list_dependencies: Set<string>;
29+
keypath: string;
30+
elements: Identifier[];
31+
render: () => void;
32+
}
33+
2534
export default class Renderer {
2635
component: Component; // TODO Maybe Renderer shouldn't know about Component?
2736
options: CompileOptions;
@@ -33,7 +42,7 @@ export default class Renderer {
3342
blocks: Array<Block | Node | Node[]> = [];
3443
readonly: Set<string> = new Set();
3544
meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
36-
binding_groups: Map<string, { binding_group: (to_reference?: boolean) => Node; is_context: boolean; contexts: string[]; index: number; keypath: string }> = new Map();
45+
binding_groups: Map<string, BindingGroup> = new Map();
3746

3847
block: Block;
3948
fragment: FragmentWrapper;
@@ -64,10 +73,6 @@ export default class Renderer {
6473
this.add_to_context('#slots');
6574
}
6675

67-
if (this.binding_groups.size > 0) {
68-
this.add_to_context('$$binding_groups');
69-
}
70-
7176
// main block
7277
this.block = new Block({
7378
renderer: this,

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
8585

8686
if (node.name === 'value') {
8787
handle_select_value_binding(this, node.dependencies);
88+
this.parent.has_dynamic_value = true;
8889
}
8990
}
9091

@@ -180,6 +181,17 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
180181
`;
181182
}
182183

184+
if (this.node.name === 'value' && dependencies.length > 0) {
185+
if (this.parent.bindings.some(binding => binding.node.name === 'group')) {
186+
this.parent.dynamic_value_condition = block.get_unique_name('value_has_changed');
187+
block.add_variable(this.parent.dynamic_value_condition, x`false`);
188+
updater = b`
189+
${updater}
190+
${this.parent.dynamic_value_condition} = true;
191+
`;
192+
}
193+
}
194+
183195
if (dependencies.length > 0) {
184196
const condition = this.get_dom_update_conditions(block, block.renderer.dirty(dependencies));
185197

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

Lines changed: 73 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import InlineComponentWrapper from '../InlineComponent';
55
import get_object from '../../../utils/get_object';
66
import replace_object from '../../../utils/replace_object';
77
import Block from '../../Block';
8-
import Renderer from '../../Renderer';
8+
import Renderer, { BindingGroup } from '../../Renderer';
99
import flatten_reference from '../../../utils/flatten_reference';
1010
import { Node, Identifier } from 'estree';
1111
import add_to_set from '../../../utils/add_to_set';
@@ -26,6 +26,7 @@ export default class BindingWrapper {
2626
snippet: Node;
2727
is_readonly: boolean;
2828
needs_lock: boolean;
29+
binding_group: BindingGroup;
2930

3031
constructor(block: Block, node: Binding, parent: ElementWrapper | InlineComponentWrapper) {
3132
this.node = node;
@@ -45,6 +46,10 @@ export default class BindingWrapper {
4546

4647
this.object = get_object(this.node.expression.node).name;
4748

49+
if (this.node.name === 'group') {
50+
this.binding_group = get_binding_group(parent.renderer, this, block);
51+
}
52+
4853
// view to model
4954
this.handler = get_event_handler(this, parent.renderer, block, this.object, this.node.raw_expression);
5055

@@ -67,6 +72,10 @@ export default class BindingWrapper {
6772
}
6873
});
6974

75+
if (this.binding_group) {
76+
this.binding_group.list_dependencies.forEach(dep => dependencies.add(dep));
77+
}
78+
7079
return dependencies;
7180
}
7281

@@ -105,6 +114,7 @@ export default class BindingWrapper {
105114

106115
const update_conditions: any[] = this.needs_lock ? [x`!${lock}`] : [];
107116
const mount_conditions: any[] = [];
117+
let update_or_condition: any = null;
108118

109119
const dependency_array = Array.from(this.get_dependencies());
110120

@@ -142,33 +152,12 @@ export default class BindingWrapper {
142152
switch (this.node.name) {
143153
case 'group':
144154
{
145-
const { binding_group, is_context, contexts, index, keypath } = get_binding_group(parent.renderer, this.node, block);
146-
147155
block.renderer.add_to_context('$$binding_groups');
156+
this.binding_group.elements.push(this.parent.var);
148157

149-
if (is_context && !block.binding_group_initialised.has(keypath)) {
150-
if (contexts.length > 1) {
151-
let binding_group = x`${block.renderer.reference('$$binding_groups')}[${index}]`;
152-
for (const name of contexts.slice(0, -1)) {
153-
binding_group = x`${binding_group}[${block.renderer.reference(name)}]`;
154-
block.chunks.init.push(
155-
b`${binding_group} = ${binding_group} || [];`
156-
);
157-
}
158-
}
159-
block.chunks.init.push(
160-
b`${binding_group(true)} = [];`
161-
);
162-
block.binding_group_initialised.add(keypath);
158+
if ((this.parent as ElementWrapper).has_dynamic_value) {
159+
update_or_condition = (this.parent as ElementWrapper).dynamic_value_condition;
163160
}
164-
165-
block.chunks.hydrate.push(
166-
b`${binding_group(true)}.push(${parent.var});`
167-
);
168-
169-
block.chunks.destroy.push(
170-
b`${binding_group(true)}.splice(${binding_group(true)}.indexOf(${parent.var}), 1);`
171-
);
172161
break;
173162
}
174163

@@ -214,7 +203,8 @@ export default class BindingWrapper {
214203

215204
if (update_dom) {
216205
if (update_conditions.length > 0) {
217-
const condition = update_conditions.reduce((lhs, rhs) => x`${lhs} && ${rhs}`);
206+
let condition = update_conditions.reduce((lhs, rhs) => x`${lhs} && ${rhs}`);
207+
if (update_or_condition) condition = x`${update_or_condition} || (${condition})`;
218208

219209
block.chunks.update.push(b`
220210
if (${condition}) {
@@ -279,7 +269,8 @@ function get_dom_updater(
279269
return b`${element.var}.${binding.node.name} = ${binding.snippet};`;
280270
}
281271

282-
function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
272+
function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: Block) {
273+
const value = binding.node;
283274
const { parts } = flatten_reference(value.raw_expression);
284275
let keypath = parts.join('.');
285276

@@ -314,41 +305,75 @@ function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
314305
contexts.push(name);
315306
}
316307

308+
// create a global binding_group across blocks
317309
if (!renderer.binding_groups.has(keypath)) {
318310
const index = renderer.binding_groups.size;
311+
// the bind:group depends on the list in the {#each} block as well
312+
// as reordering (removing and adding back to the DOM) may affect the value
313+
const list_dependencies = new Set<string>();
314+
let parent = value.parent;
315+
while (parent) {
316+
if (parent.type === 'EachBlock') {
317+
for (const dep of parent.expression.dynamic_dependencies()) {
318+
list_dependencies.add(dep);
319+
}
320+
}
321+
parent = parent.parent;
322+
}
323+
324+
const elements = [];
319325

320326
contexts.forEach(context => {
321327
renderer.add_to_context(context, true);
322328
});
323329

324330
renderer.binding_groups.set(keypath, {
325-
binding_group: (to_reference: boolean = false) => {
326-
let binding_group = '$$binding_groups';
327-
let _secondary_indexes = contexts;
331+
binding_group: () => {
332+
let obj = x`$$binding_groups[${index}]`;
328333

329-
if (to_reference) {
330-
binding_group = block.renderer.reference(binding_group);
331-
_secondary_indexes = _secondary_indexes.map(name => block.renderer.reference(name));
332-
}
333-
334-
if (_secondary_indexes.length > 0) {
335-
let obj = x`${binding_group}[${index}]`;
336-
_secondary_indexes.forEach(secondary_index => {
334+
if (contexts.length > 0) {
335+
contexts.forEach(secondary_index => {
337336
obj = x`${obj}[${secondary_index}]`;
338337
});
339-
return obj;
340-
} else {
341-
return x`${binding_group}[${index}]`;
342338
}
339+
return obj;
343340
},
344-
is_context: contexts.length > 0,
345341
contexts,
346-
index,
347-
keypath
342+
list_dependencies,
343+
keypath,
344+
elements,
345+
render() {
346+
const local_name = block.get_unique_name('binding_group');
347+
const binding_group = block.renderer.reference('$$binding_groups');
348+
block.add_variable(local_name);
349+
if (contexts.length > 0) {
350+
const indexes = { type: 'ArrayExpression', elements: contexts.map(name => block.renderer.reference(name)) };
351+
block.chunks.init.push(
352+
b`${local_name} = @init_binding_group_dynamic(${binding_group}[${index}], ${indexes})`
353+
);
354+
block.chunks.update.push(
355+
b`if (${block.renderer.dirty(Array.from(list_dependencies))}) ${local_name}.u(${indexes})`
356+
);
357+
} else {
358+
block.chunks.init.push(
359+
b`${local_name} = @init_binding_group(${binding_group}[${index}])`
360+
);
361+
}
362+
block.chunks.hydrate.push(
363+
b`${local_name}.p(${elements})`
364+
);
365+
block.chunks.destroy.push(
366+
b`${local_name}.r()`
367+
);
368+
}
348369
});
349370
}
350371

351-
return renderer.binding_groups.get(keypath);
372+
// register the binding_group for the block
373+
const binding_group = renderer.binding_groups.get(keypath);
374+
block.binding_groups.add(binding_group);
375+
376+
return binding_group;
352377
}
353378

354379
function get_event_handler(
@@ -386,7 +411,7 @@ function get_event_handler(
386411
}
387412
}
388413

389-
const value = get_value_from_dom(renderer, binding.parent, binding, block, contextual_dependencies);
414+
const value = get_value_from_dom(renderer, binding.parent, binding, contextual_dependencies);
390415

391416
const mutation = b`
392417
${lhs} = ${value};
@@ -402,10 +427,9 @@ function get_event_handler(
402427
}
403428

404429
function get_value_from_dom(
405-
renderer: Renderer,
430+
_renderer: Renderer,
406431
element: ElementWrapper | InlineComponentWrapper,
407432
binding: BindingWrapper,
408-
block: Block,
409433
contextual_dependencies: Set<string>
410434
) {
411435
const { node } = element;
@@ -427,7 +451,7 @@ function get_value_from_dom(
427451
// <input type='checkbox' bind:group='foo'>
428452
if (name === 'group') {
429453
if (type === 'checkbox') {
430-
const { binding_group, contexts } = get_binding_group(renderer, binding.node, block);
454+
const { binding_group, contexts } = binding.binding_group;
431455
add_to_set(contextual_dependencies, contexts);
432456
return x`@get_binding_group_value(${binding_group()}, this.__value, this.checked)`;
433457
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ export default class ElementWrapper extends Wrapper {
163163
has_dynamic_attribute: boolean;
164164

165165
select_binding_dependencies?: Set<string>;
166+
has_dynamic_value: boolean;
167+
dynamic_value_condition: any;
166168

167169
var: any;
168170
void: boolean;

0 commit comments

Comments
 (0)