fix select/option stuff

adjust-boundary-error-message
S. Elliott Johnson 6 days ago
parent d761749f93
commit 259d286f14

@ -70,7 +70,7 @@ export function RegularElement(node, context) {
)
) {
const child = node.fragment.nodes[0];
node.attributes.push(create_attribute('value', child.start, child.end, [child]));
node.metadata.synthetic_value_node = child;
}
const binding = context.state.scope.get(node.name);

@ -11,7 +11,7 @@ import {
import { is_ignored } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { is_custom_element_node } from '../../../nodes.js';
import { create_attribute, is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js';
import {
@ -392,10 +392,25 @@ export function RegularElement(node, context) {
}
if (!has_spread && needs_special_value_handling) {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
break;
if (node.metadata.synthetic_value_node) {
const synthetic_node = node.metadata.synthetic_value_node;
const synthetic_attribute = create_attribute(
'value',
synthetic_node.start,
synthetic_node.end,
[synthetic_node]
);
// TODO idk if necessary
synthetic_attribute.metadata.synthetic_option_value = true;
// this node is an `option` that didn't have a `value` attribute, but had
// a single-expression child, so we synthesize a value for it
build_element_special_value_attribute(node.name, node_id, synthetic_attribute, context);
} else {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
break;
}
}
}
}
@ -646,7 +661,10 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
const evaluated = context.state.scope.evaluate(value);
const assignment = b.assignment('=', b.member(node_id, '__value'), value);
const inner_assignment = b.assignment(
const is_synthetic_option =
element === 'option' && attribute.metadata.synthetic_option_value === true;
const set_value_assignment = b.assignment(
'=',
b.member(node_id, 'value'),
evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal(''))
@ -655,14 +673,16 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
const update = b.stmt(
is_select_with_value
? b.sequence([
inner_assignment,
set_value_assignment,
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.call('$.select_option', node_id, value)
])
: inner_assignment
: is_synthetic_option
? assignment
: set_value_assignment
);
if (has_state) {

@ -143,22 +143,33 @@ export function RegularElement(node, context) {
attribute.name === 'value')
)
) {
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
state.template.push(
b.stmt(
b.call(
'$.valueless_option',
b.id('$$payload'),
b.arrow(
[b.id('$$payload')],
b.block([...inner_state.init, ...build_template(inner_state.template)]),
context.state.analysis.has_blocking_await
if (node.metadata.synthetic_value_node) {
state.template.push(
b.stmt(
b.call(
'$.simple_valueless_option',
b.id('$$payload'),
node.metadata.synthetic_value_node.expression
)
)
)
);
);
} else {
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
state.template.push(
b.stmt(
b.call(
'$.valueless_option',
b.id('$$payload'),
b.arrow(
[b.id('$$payload')],
b.block([...inner_state.init, ...build_template(inner_state.template)]),
context.state.analysis.has_blocking_await
)
)
)
);
}
} else if (body !== null) {
// if this is a `<textarea>` value or a contenteditable binding, we only add
// the body if the attribute/binding is falsy

@ -348,6 +348,8 @@ export namespace AST {
has_spread: boolean;
scoped: boolean;
path: SvelteNode[];
/** Synthetic value attribute for <option> with single expression child, used for client-only handling */
synthetic_value_node: ExpressionTag | null;
};
}
@ -541,6 +543,8 @@ export namespace AST {
delegated: null | DelegatedEvent;
/** May be `true` if this is a `class` attribute that needs `clsx` */
needs_clsx: boolean;
/** true if this attribute was synthesized for client-only option value handling */
synthetic_option_value?: boolean;
};
}

@ -613,19 +613,23 @@ export function derived(fn) {
/**
*
* @param {Payload} payload
* @param {*} value
* @param {unknown} value
*/
export function maybe_selected(payload, value) {
return value === payload.local.select_value ? ' selected' : '';
}
/**
* When an `option` element has no `value` attribute, we need to treat the child
* content as its `value` to determine whether we should apply the `selected` attribute.
* This has to be done at runtime, for hopefully obvious reasons. It is also complicated,
* for sad reasons.
* @param {Payload} payload
* @param {(payload: Payload) => void | Promise<void>} children
* @param {((payload: Payload) => void | Promise<void>)} children
* @returns {void}
*/
export function valueless_option(payload, children) {
var i = payload.length;
const i = payload.length;
// prior to children, `payload` has some combination of string/unresolved payload that ends in `<option ...>`
payload.child(children);
@ -650,3 +654,23 @@ export function valueless_option(payload, children) {
}
});
}
/**
* In the special case where an `option` element has no `value` attribute but
* the children of the `option` element are a single expression, we can simplify
* by running the children and passing the resulting value, which means
* we don't have to do all of the same parsing nonsense. It also means we can avoid
* coercing everything to a string.
* @param {Payload} payload
* @param {unknown} child_value
*/
export function simple_valueless_option(payload, child_value) {
if (child_value === payload.local.select_value) {
payload.compact({
start: payload.length - 1,
fn: (content) => ({ body: content.body.slice(0, -1) + ' selected>', head: content.head })
});
}
payload.push(escape_html(child_value));
}

@ -8,9 +8,9 @@ export default test({
ssrHtml: `
<select>
<option value="1">1</option>
<option selected value="2">2</option>
<option value="3">3</option>
<option>1</option>
<option selected>2</option>
<option>3</option>
</select>
<p>foo: 2</p>
@ -21,9 +21,9 @@ export default test({
target.innerHTML,
`
<select>
<option value="1">1</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}value="2">2</option>
<option value="3">3</option>
<option>1</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}>2</option>
<option>3</option>
</select>
<p>foo: 2</p>
@ -47,9 +47,9 @@ export default test({
target.innerHTML,
`
<select>
<option value="1">1</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}value="2">2</option>
<option value="3">3</option>
<option>1</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}>2</option>
<option>3</option>
</select>
<p>foo: 3</p>

@ -8,9 +8,9 @@ export default test({
<select>
<option disabled=''>x</option>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
<option>a</option>
<option>b</option>
<option>c</option>
</select>
`,

@ -28,9 +28,9 @@ export default test({
target.innerHTML,
`
<select>
<option value="one">one</option>
<option value="two">two</option>
<option value="three">three</option>
<option>one</option>
<option>two</option>
<option>three</option>
</select>
<p>selected: two</p>
`

@ -28,9 +28,9 @@ export default test({
target.innerHTML,
`
<select>
<option value='one'>one</option>
<option value='two'>two</option>
<option value='three'>three</option>
<option>one</option>
<option>two</option>
<option>three</option>
</select>
<p>selected: two</p>
`

@ -28,9 +28,9 @@ export default test({
target.innerHTML,
`
<select>
<option value="one">one</option>
<option value="two">two</option>
<option value="three">three</option>
<option>one</option>
<option>two</option>
<option>three</option>
</select>
<p>selected: two</p>
`

@ -6,9 +6,9 @@ export default test({
<p>selected: </p>
<select>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<p>selected: </p>
@ -37,9 +37,9 @@ export default test({
<p>selected: a</p>
<select>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<p>selected: a</p>
@ -59,9 +59,9 @@ export default test({
<p>selected: d</p>
<select>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<p>selected: d</p>
@ -78,9 +78,9 @@ export default test({
<p>selected: b</p>
<select>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<p>selected: b</p>
@ -100,9 +100,9 @@ export default test({
<p>selected: </p>
<select>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<p>selected: </p>

@ -4,7 +4,7 @@ import { ok, test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
html: `<p>selected: a</p><select><option value="a">a</option><option value="b">b</option><option value="c">c</option></select>`,
html: `<p>selected: a</p><select><option>a</option><option>b</option><option>c</option></select>`,
async test({ assert, component, target }) {
const select = target.querySelector('select');
@ -29,7 +29,7 @@ export default test({
// model of selected value should be kept around, even if it is not in the list
assert.htmlEqual(
target.innerHTML,
`<p>selected: a</p><select><option value="b">b</option><option value="c">c</option></select>`
`<p>selected: a</p><select><option>b</option><option>c</option></select>`
);
}
});

Loading…
Cancel
Save