pull/16797/head
Rich Harris 6 days ago
parent 6963de7ae8
commit 6eac9ed41e

@ -141,41 +141,53 @@ export function RegularElement(node, context) {
node.name === 'option' &&
!node.attributes.some(
(attribute) =>
attribute.type === 'SpreadAttribute' ||
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value')
(attribute.type === 'Attribute' && attribute.name === 'value') ||
attribute.type === 'SpreadAttribute'
)
) {
const attributes = build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context,
optimiser.transform
);
let body;
if (node.metadata.synthetic_value_node) {
state.template.push(
b.stmt(
b.call(
'$.simple_valueless_option',
b.id('$$renderer'),
b.thunk(
node.metadata.synthetic_value_node.expression,
node.metadata.synthetic_value_node.metadata.expression.has_await
)
)
)
body = optimiser.transform(
node.metadata.synthetic_value_node.expression,
node.metadata.synthetic_value_node.metadata.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('$$renderer'),
b.arrow(
[b.id('$$renderer')],
b.block([...inner_state.init, ...build_template(inner_state.template)])
)
)
)
body = b.arrow(
[b.id('$$renderer')],
b.block([...state.init, ...build_template(inner_state.template)])
);
}
} else if (body !== null) {
const statement = b.stmt(b.call('$$renderer.option', attributes, body));
if (optimiser.expressions.length > 0) {
context.state.template.push(
call_child_renderer(b.block([optimiser.apply(), ...state.init, statement]), true)
);
} else {
context.state.template.push(...state.init, statement);
}
return;
}
if (body !== null) {
// if this is a `<textarea>` value or a contenteditable binding, we only add
// the body if the attribute/binding is falsy
const inner_state = { ...state, template: [], init: [] };

@ -503,86 +503,6 @@ export function maybe_selected(renderer, value) {
return value === renderer.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 {Renderer} renderer
* @param {((renderer: Renderer) => void | Promise<void>)} children
* @returns {void}
*/
export function valueless_option(renderer, children) {
const i = renderer.length;
// prior to children, `renderer` has some combination of string/unresolved renderer that ends in `<option ...>`
renderer.child(children);
// post-children, `renderer` has child content, possibly also with some number of hydration comments.
// we can compact this last chunk of content to see if it matches the select value...
renderer.compact({
start: i,
fn: (content) => {
if (content.body.replace(/<!---->/g, '') === renderer.local.select_value) {
// ...and if it does match the select value, we can compact the part of the renderer representing the `<option ...>`
// to add the `selected` attribute to the end.
renderer.compact({
start: i - 1,
end: i,
fn: (content) => {
return { body: content.body.slice(0, -1) + ' selected>', head: content.head };
}
});
}
return content;
}
});
}
/**
* 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 {Renderer} renderer
* @param {(() => unknown)} child
*/
export function simple_valueless_option(renderer, child) {
const result = child();
/**
* @param {AccumulatedContent} content
* @param {unknown} child_value
* @returns {AccumulatedContent}
*/
const mark_selected = (content, child_value) => {
if (child_value === renderer.local.select_value) {
return { body: content.body.slice(0, -1) + ' selected>', head: content.head };
}
return content;
};
renderer.compact({
start: renderer.length - 1,
fn: (content) => {
if (result instanceof Promise) {
return result.then((child_value) => mark_selected(content, child_value));
}
return mark_selected(content, result);
}
});
renderer.child((child_renderer) => {
if (result instanceof Promise) {
return result.then((child_value) => {
child_renderer.push(escape_html(child_value));
});
}
child_renderer.push(escape_html(result));
});
}
/**
* Since your document can only have one `title`, we have to have some sort of algorithm for determining
* which one "wins". To do this, we perform a depth-first comparison of where the title was encountered --

@ -172,6 +172,56 @@ export class Renderer {
this.push('</select>');
}
/**
* @param {Record<string, any>} attrs
* @param {any} body
*/
option(attrs, body) {
this.#out.push(`<option${attributes(attrs)}`);
/**
* @param {Renderer} renderer
* @param {any} value
* @param {{ head?: string, body: any }} content
*/
const close = (renderer, value, { head, body }) => {
if ('value' in attrs) {
value = attrs.value;
}
if (value === this.local.select_value) {
renderer.#out.push(' selected');
}
renderer.#out.push(`>${body}</option>`);
// super edge case, but may as well handle it
if (head) {
renderer.head((child) => child.push(head));
}
};
if (typeof body === 'function') {
this.child((renderer) => {
const r = new Renderer(this.global, this);
body(r);
const content = { head: '', body: '' };
if (this.global.mode === 'async') {
return Renderer.#collect_content_async([r], 'body', content).then(() => {
close(renderer, content.body.replaceAll('<!---->', ''), content);
});
} else {
Renderer.#collect_content([r], 'body', content);
close(renderer, content.body.replaceAll('<!---->', ''), content);
}
});
} else {
close(this, body, { body });
}
}
/**
* @param {string | (() => Promise<string>)} content
*/

Loading…
Cancel
Save