use crate::ast::{DeclarativeSelectOption, DeclarativeTypographyStyle, DeclarativeUiNode};
use crate::error::DeclarativeUiAssetLoadError;
use crate::parser::{
DeclarativeStateSpec, attr, attr_error, bound_attr, dsl_error, element_children, model_attr,
parse_binding_path_expr, parse_bool_or_condition_attr, parse_class_bindings, parse_conditional,
parse_event_bindings, parse_mustache_expr, parse_node_style, parse_node_style_binding,
parse_ref_binding, parse_show_attr, parse_state_visual_styles, parse_text_content,
parse_utility_class_patch, parse_v_for, parse_visual_style, reject_legacy_attrs,
reject_legacy_bind_attrs, reject_style_attrs, reject_style_attrs_except,
};
use roxmltree::Node as XmlNode;
use std::collections::BTreeMap;
pub(crate) fn parse_declarative_select_node(
node: XmlNode<'_, '_>,
state_specs: &BTreeMap<String, DeclarativeStateSpec>,
) -> Result<DeclarativeUiNode, DeclarativeUiAssetLoadError> {
reject_legacy_attrs(node, &["text", "key", "bind-text", "bind-key", "visible"])?;
reject_legacy_bind_attrs(node)?;
reject_style_attrs_except(node, &["style"])?;
if attr(node, "multiple").is_some() {
return Err(attr_error(
node,
"multiple",
attr(node, "multiple").unwrap_or_default(),
"multiple select is not supported",
));
}
let class_patch = parse_utility_class_patch(node)?;
let (value, value_binding, model_binding) = parse_select_value(node)?;
let options = parse_select_options(node, state_specs)?;
let (disabled, disabled_expr) = parse_bool_or_condition_attr(node, "disabled", state_specs)?;
let show_expr = parse_show_attr(node, state_specs)?;
Ok(DeclarativeUiNode::Select {
node_id: String::new(),
name: attr(node, "name").unwrap_or_default().to_string(),
class: attr(node, "class").unwrap_or_default().to_string(),
class_bindings: parse_class_bindings(node, state_specs)?,
conditional: parse_conditional(node, state_specs)?,
value,
value_binding,
model_binding,
ref_binding: parse_ref_binding(node)?,
event_bindings: parse_event_bindings(node)?,
style_binding: parse_node_style_binding(node)?,
options,
node_override: Some(parse_node_style(node)?),
visual_style: parse_visual_style(node)?,
state_visual_styles: parse_state_visual_styles(node)?,
disabled,
disabled_expr,
show_expr,
label_typography_override: DeclarativeTypographyStyle {
family_role: class_patch
.font_family_role
.map(crate::parser::font_family_role_from_utility),
size: class_patch.font_size,
weight: class_patch.font_weight,
font_style: class_patch.font_style.map(crate::parser::font_style_from_utility),
line_height: class_patch.line_height,
letter_spacing_em: class_patch.letter_spacing_em,
text_transform: class_patch
.text_transform
.map(crate::parser::text_transform_from_utility),
},
})
}
fn parse_select_value(
node: XmlNode<'_, '_>,
) -> Result<(String, Option<String>, Option<String>), DeclarativeUiAssetLoadError> {
let value_attr = attr(node, "value").unwrap_or_default();
let value_binding = bound_attr(node, "value")
.map(|expr| parse_binding_path_expr(node, ":value", expr))
.transpose()?;
let model_binding = model_attr(node)
.map(|expr| parse_binding_path_expr(node, "v-model", expr))
.transpose()?;
if value_binding.is_some() || model_binding.is_some() {
return Ok((String::new(), value_binding, model_binding));
}
if let Some(expr) = parse_mustache_expr(value_attr) {
return Err(attr_error(
node,
"value",
expr,
"use :value or v-model for bound values",
));
}
Ok((value_attr.to_string(), None, None))
}
fn parse_select_options(
node: XmlNode<'_, '_>,
state_specs: &BTreeMap<String, DeclarativeStateSpec>,
) -> Result<Vec<DeclarativeSelectOption>, DeclarativeUiAssetLoadError> {
let mut options = Vec::new();
let mut selected_count = 0usize;
for child in element_children(node) {
if !child.has_tag_name("option") {
return Err(dsl_error(child, "<select> accepts only <option> children"));
}
let option = parse_select_option(child, state_specs)?;
if option.selected && option.repeat.is_none() {
selected_count += 1;
}
options.push(option);
}
if attr(node, "value").is_none() && selected_count > 1 {
return Err(dsl_error(
node,
"<select> accepts at most one selected <option> when value is omitted",
));
}
Ok(options)
}
fn parse_select_option(
node: XmlNode<'_, '_>,
state_specs: &BTreeMap<String, DeclarativeStateSpec>,
) -> Result<DeclarativeSelectOption, DeclarativeUiAssetLoadError> {
reject_legacy_attrs(node, &["visible", "text", "key", "bind-text", "bind-key"])?;
reject_legacy_bind_attrs(node)?;
reject_style_attrs(node)?;
if attr(node, "v-on-click").is_some() {
return Err(attr_error(
node,
"@click",
attr(node, "v-on-click").unwrap_or_default(),
"<option> does not support @click; use <select @change>",
));
}
if !parse_event_bindings(node)?.is_empty() {
return Err(dsl_error(
node,
"<option> does not support event handlers; use <select @change>",
));
}
let selected = parse_native_bool_attr(node, "selected")?;
let (disabled, disabled_expr) = parse_bool_or_condition_attr(node, "disabled", state_specs)?;
let (value, value_binding) = parse_option_value(node)?;
Ok(DeclarativeSelectOption {
value,
value_binding,
content: parse_text_content(node)?,
selected,
disabled,
disabled_expr,
conditional: parse_conditional(node, state_specs)?,
repeat: attr(node, "v-for").map(|_| parse_v_for(node)).transpose()?,
})
}
fn parse_option_value(
node: XmlNode<'_, '_>,
) -> Result<(Option<String>, Option<String>), DeclarativeUiAssetLoadError> {
let value_attr = attr(node, "value").unwrap_or_default();
if let Some(expr) = bound_attr(node, "value") {
return Ok((None, Some(parse_binding_path_expr(node, ":value", expr)?)));
}
if let Some(expr) = parse_mustache_expr(value_attr) {
return Ok((None, Some(parse_binding_path_expr(node, "value", expr)?)));
}
Ok((attr(node, "value").map(str::to_string), None))
}
#[cfg(test)]
mod tests {
use crate::{
DeclarativeActionSpec, DeclarativeUiNode, parse_declarative_ui_asset, set_action_resolver,
};
fn install_test_action_resolver() {
set_action_resolver(|name| match name {
"settingInput" => Some(DeclarativeActionSpec {
action_id: "setting.input",
param_names: vec!["key"],
}),
"settingChange" => Some(DeclarativeActionSpec {
action_id: "setting.change",
param_names: vec!["key"],
}),
"uiScroll" => Some(DeclarativeActionSpec {
action_id: "ui.scroll",
param_names: vec!["key"],
}),
"uiWheel" => Some(DeclarativeActionSpec {
action_id: "ui.wheel",
param_names: vec!["key"],
}),
_ => None,
});
}
#[test]
fn select_option_supports_v_for_and_bound_attrs() {
let asset = parse_declarative_ui_asset(
r#"
<template>
<select name="language" :value="current">
<option v-for="entry in options" :value="entry.value" :disabled="entry.disabled">{{ entry.text }}</option>
</select>
</template>
"#,
)
.expect("select asset should parse");
let DeclarativeUiNode::Select { options, .. } = asset.root else {
panic!("expected select root");
};
let option = options.first().expect("expected option");
assert_eq!(option.value_binding.as_deref(), Some("entry.value"));
assert!(option.disabled_expr.is_some());
assert!(option.repeat.is_some());
}
#[test]
fn select_supports_v_model() {
install_test_action_resolver();
let asset = parse_declarative_ui_asset(
r#"
<template>
<select v-model="settings.language" @change="settingChange(settings.key)">
<option value="en">English</option>
</select>
</template>
"#,
)
.expect("select should parse");
let DeclarativeUiNode::Select {
model_binding,
event_bindings,
..
} = asset.root
else {
panic!("expected select root");
};
assert_eq!(model_binding.as_deref(), Some("settings.language"));
assert_eq!(event_bindings.len(), 1);
}
}
fn parse_native_bool_attr(
node: XmlNode<'_, '_>,
name: &str,
) -> Result<bool, DeclarativeUiAssetLoadError> {
let Some(raw) = attr(node, name) else {
return Ok(false);
};
match raw.trim() {
"" | "true" => Ok(true),
"false" => Ok(false),
_ => Err(attr_error(node, name, raw, "expected boolean attribute")),
}
}