Skip to content

Commit 6289dfc

Browse files
authored
fix <select> one-way bind when options change (#5054)
1 parent dc73b73 commit 6289dfc

File tree

7 files changed

+95
-17
lines changed

7 files changed

+95
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
* Fix handling of `this` in inline function expressions in the template ([#5033](https://github.com/sveltejs/svelte/issues/5033))
6+
* Update `<select>` with one-way `value` binding when the available `<option>`s change ([#5051](https://github.com/sveltejs/svelte/issues/5051))
67

78
## 3.23.2
89

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

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { string_literal } from '../../../utils/stringify';
66
import { b, x } from 'code-red';
77
import Expression from '../../../nodes/shared/Expression';
88
import Text from '../../../nodes/Text';
9+
import handle_select_value_binding from './handle_select_value_binding';
910

1011
export default class AttributeWrapper {
1112
node: Attribute;
@@ -36,6 +37,10 @@ export default class AttributeWrapper {
3637
});
3738
}
3839
}
40+
41+
if (node.name === 'value') {
42+
handle_select_value_binding(this, node.dependencies);
43+
}
3944
}
4045
}
4146

@@ -74,7 +79,7 @@ export default class AttributeWrapper {
7479

7580
const is_legacy_input_type = element.renderer.component.compile_options.legacy && name === 'type' && this.parent.node.name === 'input';
7681

77-
const dependencies = this.node.get_dependencies();
82+
const dependencies = this.get_dependencies();
7883
const value = this.get_value(block);
7984

8085
const is_src = this.node.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
@@ -83,7 +88,7 @@ export default class AttributeWrapper {
8388

8489
const is_input_value = name === 'value' && element.node.name === 'input';
8590

86-
const should_cache = is_src || this.node.should_cache() || is_select_value_attribute; // TODO is this necessary?
91+
const should_cache = is_src || this.node.should_cache();
8792

8893
const last = should_cache && block.get_unique_name(
8994
`${element.var.name}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
@@ -104,13 +109,12 @@ export default class AttributeWrapper {
104109
const is_multiple_select = element.node.get_static_attribute_value('multiple');
105110

106111
if (is_multiple_select) {
107-
updater = b`@select_options(${element.var}, ${last});`;
112+
updater = b`@select_options(${element.var}, ${value});`;
108113
} else {
109-
updater = b`@select_option(${element.var}, ${last});`;
114+
updater = b`@select_option(${element.var}, ${value});`;
110115
}
111116

112117
block.chunks.mount.push(b`
113-
${last} = ${value};
114118
${updater}
115119
`);
116120
} else if (is_src) {
@@ -168,10 +172,26 @@ export default class AttributeWrapper {
168172
const update_value = b`${element.var}.value = ${element.var}.__value;`;
169173

170174
block.chunks.hydrate.push(update_value);
171-
if (this.node.get_dependencies().length > 0) block.chunks.update.push(update_value);
175+
if (dependencies.length > 0) block.chunks.update.push(update_value);
172176
}
173177
}
174178

179+
get_dependencies() {
180+
const node_dependencies = this.node.get_dependencies();
181+
const dependencies = new Set(node_dependencies);
182+
183+
node_dependencies.forEach((prop: string) => {
184+
const indirect_dependencies = this.parent.renderer.component.indirect_dependencies.get(prop);
185+
if (indirect_dependencies) {
186+
indirect_dependencies.forEach(indirect_dependency => {
187+
dependencies.add(indirect_dependency);
188+
});
189+
}
190+
});
191+
192+
return Array.from(dependencies);
193+
}
194+
175195
get_metadata() {
176196
if (this.parent.node.namespace) return null;
177197
const metadata = attribute_lookup[fix_attribute_casing(this.node.name)];

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import flatten_reference from '../../../utils/flatten_reference';
1010
import { Node, Identifier } from 'estree';
1111
import add_to_set from '../../../utils/add_to_set';
1212
import mark_each_block_bindings from '../shared/mark_each_block_bindings';
13+
import handle_select_value_binding from './handle_select_value_binding';
1314

1415
export default class BindingWrapper {
1516
node: Binding;
@@ -35,12 +36,8 @@ export default class BindingWrapper {
3536
block.add_dependencies(dependencies);
3637

3738
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
38-
if (parent.node.name === 'select') {
39-
(parent as ElementWrapper).select_binding_dependencies = dependencies;
40-
dependencies.forEach((prop: string) => {
41-
parent.renderer.component.indirect_dependencies.set(prop, new Set());
42-
});
43-
}
39+
40+
handle_select_value_binding(this, dependencies);
4441

4542
if (node.is_contextual) {
4643
mark_each_block_bindings(this.parent, this.node);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import AttributeWrapper from "./Attribute";
2+
import BindingWrapper from "./Binding";
3+
import ElementWrapper from "./index";
4+
5+
export default function handle_select_value_binding(
6+
attr: AttributeWrapper | BindingWrapper,
7+
dependencies: Set<string>
8+
) {
9+
const { parent } = attr;
10+
if (parent.node.name === "select") {
11+
(parent as ElementWrapper).select_binding_dependencies = dependencies;
12+
dependencies.forEach((prop: string) => {
13+
parent.renderer.component.indirect_dependencies.set(prop, new Set());
14+
});
15+
}
16+
}

test/js/samples/select-dynamic-value/expected.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ function create_fragment(ctx) {
1515
let select;
1616
let option0;
1717
let option1;
18-
let select_value_value;
1918

2019
return {
2120
c() {
@@ -33,12 +32,11 @@ function create_fragment(ctx) {
3332
insert(target, select, anchor);
3433
append(select, option0);
3534
append(select, option1);
36-
select_value_value = /*current*/ ctx[0];
37-
select_option(select, select_value_value);
35+
select_option(select, /*current*/ ctx[0]);
3836
},
3937
p(ctx, [dirty]) {
40-
if (dirty & /*current*/ 1 && select_value_value !== (select_value_value = /*current*/ ctx[0])) {
41-
select_option(select, select_value_value);
38+
if (dirty & /*current*/ 1) {
39+
select_option(select, /*current*/ ctx[0]);
4240
}
4341
},
4442
i: noop,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export default {
2+
props: {
3+
items: [],
4+
selected: 'two'
5+
},
6+
7+
html: `
8+
<select></select>
9+
<p>selected: two</p>
10+
`,
11+
12+
ssrHtml: `
13+
<select value="two"></select>
14+
<p>selected: two</p>
15+
`,
16+
17+
test({ assert, component, target }) {
18+
component.items = [ 'one', 'two', 'three' ];
19+
20+
const options = target.querySelectorAll('option');
21+
assert.ok(!options[0].selected);
22+
assert.ok(options[1].selected);
23+
assert.ok(!options[2].selected);
24+
25+
assert.htmlEqual(target.innerHTML, `
26+
<select>
27+
<option value='one'>one</option>
28+
<option value='two'>two</option>
29+
<option value='three'>three</option>
30+
</select>
31+
<p>selected: two</p>
32+
`);
33+
}
34+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
export let selected;
3+
export let items;
4+
</script>
5+
6+
<select value={selected} on:blur={e => selected = e.target.value}>
7+
{#each items as item}
8+
<option>{item}</option>
9+
{/each}
10+
</select>
11+
12+
<p>selected: {selected || 'nothing'}</p>

0 commit comments

Comments
 (0)