Skip to content

Commit cdbd584

Browse files
authored
fix(web): assign static select values through DOM property (#3167)
1 parent 432b089 commit cdbd584

6 files changed

Lines changed: 103 additions & 7 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@solidjs/babel-plugin": patch
3+
"@solidjs/compiler": patch
4+
---
5+
6+
Assign static `<select value>` values through the live DOM property so the matching option is selected consistently with reactive values.

packages/babel-plugin/src/dom/element.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1221,7 +1221,10 @@ function transformAttributes(
12211221
}
12221222

12231223
// properties
1224-
if (staticValue && ChildProperties.has(key)) {
1224+
if (
1225+
staticValue &&
1226+
(ChildProperties.has(key) || (tagName === "select" && key === "value"))
1227+
) {
12251228
results.exprs.push(
12261229
t.expressionStatement(
12271230
setAttr(attribute, elem, key, staticValue as babelTypes.Expression, { tagName })
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const babel = require("@babel/core");
2+
const plugin = require("../index");
3+
4+
function compile(code) {
5+
return babel.transformSync(code, {
6+
babelrc: false,
7+
configFile: false,
8+
filename: "input.jsx",
9+
plugins: [[plugin, { moduleName: "r-dom", generate: "dom" }]]
10+
}).code;
11+
}
12+
13+
test("assigns static select values through the DOM property", () => {
14+
const code = compile(`
15+
const stringAttribute = <select value="2"><option value="2">Two</option></select>;
16+
const stringExpression = <select value={"2"}><option value="2">Two</option></select>;
17+
const numberExpression = <select value={2}><option value="2">Two</option></select>;
18+
const multipleString = <select multiple value="2"><option value="2">Two</option></select>;
19+
const multipleArray = <select multiple value={["1", "2"]}><option value="2">Two</option></select>;
20+
const dynamicChildren = <select value="2">{options()}</select>;
21+
`);
22+
23+
expect(code).not.toMatch(/_\$template\(`<select value=/);
24+
expect(code.match(/queueMicrotask/g)).toHaveLength(6);
25+
expect(code.match(/\.value = (?:"2"|2)/g)).toHaveLength(10);
26+
expect(code.match(/\.value = \["1", "2"\]/g)).toHaveLength(2);
27+
expect(code.indexOf("queueMicrotask")).toBeLessThan(code.indexOf("_$insert("));
28+
});

packages/compiler/__tests__/transform.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,29 @@ describe("@solidjs/compiler transform", () => {
182182
expect(result.code).not.toContain("</input>");
183183
});
184184

185+
it("assigns static select values through the DOM property", () => {
186+
const result = transform(
187+
`
188+
const stringAttribute = <select value="2"><option value="2">Two</option></select>;
189+
const stringExpression = <select value={"2"}><option value="2">Two</option></select>;
190+
const numberExpression = <select value={2}><option value="2">Two</option></select>;
191+
const multipleString = <select multiple value="2"><option value="2">Two</option></select>;
192+
const multipleArray = <select multiple value={["1", "2"]}><option value="2">Two</option></select>;
193+
const dynamicChildren = <select value="2">{options()}</select>;
194+
`,
195+
{
196+
filename: "static-select-value.jsx",
197+
moduleName: "r-dom"
198+
}
199+
);
200+
201+
expect(result.code).not.toMatch(/_\$template\(`<select value=/);
202+
expect(result.code.match(/queueMicrotask/g)).toHaveLength(6);
203+
expect(result.code.match(/\.value = (?:"2"|2)/g)).toHaveLength(10);
204+
expect(result.code.match(/\.value = \["1", "2"\]/g)).toHaveLength(2);
205+
expect(result.code.indexOf("queueMicrotask")).toBeLessThan(result.code.indexOf("_$insert("));
206+
});
207+
185208
it("compiles the supported simpleElements fixture subset from Babel sources", () => {
186209
const source = readFixture("simpleElements");
187210
const subset = source.slice(

packages/compiler/src/dom/attrs.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ impl<'a> AstDomTransform<'a, '_> {
165165
if has_children && plan.key == "textContent" {
166166
continue;
167167
}
168-
match self.classify_plan(&plan) {
168+
match self.classify_plan(&plan, tag_name) {
169169
PlanDisposition::Skip => {}
170170
PlanDisposition::Inline(value) => match value {
171171
None => append_bare_attribute(template, &plan.key, self.omit_attribute_spacing),
@@ -200,7 +200,7 @@ impl<'a> AstDomTransform<'a, '_> {
200200

201201
/// Pure classification of one planned attribute, mirroring Babel's
202202
/// static-vs-expression branch in the attribute loop.
203-
fn classify_plan(&self, plan: &AttrPlan<'a>) -> PlanDisposition {
203+
fn classify_plan(&self, plan: &AttrPlan<'a>, tag_name: &str) -> PlanDisposition {
204204
if self.hydratable && plan.key == "$ServerOnly" {
205205
return PlanDisposition::Skip;
206206
}
@@ -211,6 +211,7 @@ impl<'a> AstDomTransform<'a, '_> {
211211
return PlanDisposition::Skip;
212212
}
213213
let reserved = plan.style_property || plan.class_property || plan.key.starts_with("prop:");
214+
let select_value = tag_name == "select" && plan.key == "value";
214215
match &plan.value {
215216
PlanValue::None => {
216217
if reserved {
@@ -222,7 +223,7 @@ impl<'a> AstDomTransform<'a, '_> {
222223
}
223224
}
224225
PlanValue::Literal(value) => {
225-
if reserved || child_properties(&plan.key) {
226+
if reserved || child_properties(&plan.key) || select_value {
226227
PlanDisposition::Runtime
227228
} else {
228229
PlanDisposition::Inline(Some(value.clone()))
@@ -243,14 +244,14 @@ impl<'a> AstDomTransform<'a, '_> {
243244
}
244245
}
245246
Expression::StringLiteral(literal) => {
246-
if child_properties(&plan.key) {
247+
if child_properties(&plan.key) || select_value {
247248
PlanDisposition::Runtime
248249
} else {
249250
PlanDisposition::Inline(Some(literal.value.to_string()))
250251
}
251252
}
252253
Expression::NumericLiteral(literal) => {
253-
if child_properties(&plan.key) {
254+
if child_properties(&plan.key) || select_value {
254255
PlanDisposition::Runtime
255256
} else {
256257
PlanDisposition::Inline(Some(format_number(literal.value)))
@@ -586,7 +587,7 @@ impl<'a> AstDomTransform<'a, '_> {
586587
} = self.plan_attributes(attributes, tag_name)?;
587588
let mut pending: std::vec::Vec<(String, Option<String>)> = std::vec::Vec::new();
588589
for plan in &plans {
589-
match self.classify_plan(plan) {
590+
match self.classify_plan(plan, tag_name) {
590591
PlanDisposition::Skip => {}
591592
PlanDisposition::Inline(value) => pending.push((plan.key.clone(), value)),
592593
PlanDisposition::Runtime => return Ok(None),

packages/web/test/element.spec.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,41 @@ describe("Basic element attributes", () => {
3737
expect(d.innerHTML).toBe("<p>Hi</p>");
3838
});
3939

40+
test("static select value selects the matching option (#3167)", () => {
41+
const select = (
42+
<select value="2">
43+
<option value="">None</option>
44+
<option value="1">One</option>
45+
<option value="2">Two</option>
46+
</select>
47+
) as unknown as HTMLSelectElement;
48+
49+
expect(select.value).toBe("2");
50+
expect(select.selectedIndex).toBe(2);
51+
});
52+
53+
test("static select value is reapplied after dynamic children (#3167)", async () => {
54+
const options = () => [<option value="1">One</option>, <option value="2">Two</option>];
55+
const select = (<select value="2">{options()}</select>) as unknown as HTMLSelectElement;
56+
57+
await Promise.resolve();
58+
59+
expect(select.value).toBe("2");
60+
expect(select.selectedIndex).toBe(1);
61+
});
62+
63+
test("static select value selects one option in a multiple select (#3167)", () => {
64+
const select = (
65+
<select multiple value="2">
66+
<option value="1">One</option>
67+
<option value="2">Two</option>
68+
</select>
69+
) as unknown as HTMLSelectElement;
70+
71+
expect(select.multiple).toBe(true);
72+
expect(Array.from(select.options, option => option.selected)).toEqual([false, true]);
73+
});
74+
4075
test("class", () => {
4176
const classes = { first: true, second: false, "third fourth": true },
4277
d = (<div class={classes} />) as unknown as HTMLDivElement;

0 commit comments

Comments
 (0)