在Jmix中引入jQuery UI Spinner并不是简单地把一个HTML片段放到页面里。Jmix默认使用服务端驱动的组件模型,页面上的数据控件通常需要和实体属性的ValueSource建立联系,否则组件看到的只是一个静态输入框。Spinner的值变化如果只停留在客户端,服务端实体不会自动感知,Jmix的校验引擎也不会在合适的时机运行,最终就会形成界面显示正常、保存后却报错或数据丢失的情况。要解决这类问题,需要把集成拆成三层来考虑:客户端组件如何上报值、服务端如何把值写入实体、校验失败后如何把错误状态反馈给Spinner。

一、先理清组件边界:Spinner事件与Jmix值源的关系
jQuery UI Spinner的值变化来自两种路径:用户点击上下按钮触发spin事件,或者在输入框中手动输入后失焦触发change事件。这两个事件携带的值类型并不完全一致,spin回调里的ui.value可能是计算后的数值,change回调则通常需要重新读取当前输入内容。很多集成方案失败在只处理了按钮事件,导致手动输入的数字不会回传到数据模型。
Jmix侧的数据绑定建立在ValueSource之上,字段组件通过setValue方法把新值写入模型,再由ValueSource通知数据容器和实体。若缺少这一层调用,Spinner只是把显示文本改掉,数据容器中的实体属性仍然是旧值。因此第一步不是急着写校验,而是明确值从客户端到服务端的转换链路:jQuery事件读取值、客户端连接器发送值、服务端字段接收值、ValueSource写入实体、DataContext标记实体为修改。这个链路中的每一环都需要显式编码,不能依赖默认行为。
二、实现Spinner与服务端字段的双向绑定
为了在Jmix页面中复用Spinner,建议把它包装为一个标准字段组件,例如继承Vaadin的CustomField或AbstractSinglePropertyField。包装后的组件可以像普通输入框一样被Jmix的XML描述或Java代码引用,并且能获得数据绑定、只读状态、可见性控制等基础能力。下面是一个简化实现,核心是维护内部value,并为客户端调用暴露一个服务端入口。
public class JmixSpinnerField extends CustomField<Integer> {
private Integer value = 0;
public JmixSpinnerField() {
addValueChangeListener(event -> {
if (event.isFromClient()) {
// 值已经由客户端上报,触发后续校验
}
});
}
@Override
protected Integer generateModelValue() {
return value;
}
@Override
protected void setPresentationValue(Integer newPresentationValue) {
this.value = newPresentationValue;
getElement().setProperty("spinnerValue", newPresentationValue);
}
@ClientCallable
public void updateValueFromClient(int newValue) {
setValue(newValue, true);
}
}
对应的客户端连接器需要初始化jQuery UI Spinner,并把原生值变化事件转换为服务端可识别的调用。对于手写JavaScript连接器的项目,可以在元素挂载后注册事件,调用服务端发布的函数。为保证手动输入也能同步,change事件必须与spin事件一起监听。
window.kompany = window.kompany || {};
window.kompany.JmixSpinnerConnector = function(element) {
var $element = $(element);
$element.spinner({
min: element.getAttribute('data-min') || 0,
max: element.getAttribute('data-max') || 100,
step: element.getAttribute('data-step') || 1
});
function publishValue(value) {
element.$server.updateValueFromClient(Number(value));
}
$element.on('spin', function(event, ui) {
publishValue(ui.value);
});
$element.on('change', function() {
publishValue($(this).spinner('value'));
});
};
三、让校验规则在合适的时机触发
Jmix的校验通常由Bean Validation注解定义在实体字段上,例如@Min、@Max、@NotNull。数据容器提交时会调用BeanValidation校验整个实体,返回ValidationErrors。但如果只在点击保存时校验,Spinner可以在提交前保持非法值而不给出提示。更好的做法是结合值变更监听器,在字段值写入模型后立即做一次轻量级字段校验。
在SpinnerField内部可以注入BeanValidation,并在setValue后调用。下面这段代码演示了如何在值变更事件中校验当前字段,并将错误信息写回组件状态。这样做不会覆盖其他字段的错误,仍然能在提交时执行全实体校验。
@Autowired
private BeanValidation beanValidation;
public JmixSpinnerField() {
addValueChangeListener(event -> {
if (event.isFromClient()) {
validateField();
}
});
}
private void validateField() {
Object entity = getValueSource() == null ? null : getValueSource().getOwner();
if (entity == null) {
return;
}
String fieldName = getValueSource() == null ? "" : getValueSource().getProperty();
ValidationErrors errors = beanValidation.validate(entity);
if (errors.isNotEmpty() && errors.getByField(fieldName) != null) {
setErrorMessage(errors.getByField(fieldName).getMessage());
setInvalid(true);
} else {
setErrorMessage(null);
setInvalid(false);
}
}
四、处理精度、输入异常和区域设置
Spinner的step、min、max属性通常使用整数演示,但实际业务中价格、重量、比例往往需要小数。如果服务端字段类型是BigDecimal或Double,客户端到服务端的Number转换必须保留精度,否则会出现0.1被转成0.10000000000000001这类问题。应避免用float中转,而要将字符串值按服务端类型转换,或使用BigDecimal.valueOf并指定精度。区域设置还影响小数点符号,jQuery UI Spinner默认按照浏览器区域解析数字,中文和英文环境可能一个用点一个用逗号,集成时需要统一指定numberFormat或先将值规范化为纯数字字符串。
异常输入也是常见难点。当用户在Spinner输入框中粘贴非数字文本并失焦,jQuery UI Spinner可能返回NaN或空字符串。服务端如果直接setValue(NaN)会导致模型脏数据。可以在客户端publishValue之前做isFinite过滤,只有合法数字才发送;服务端字段也要防御性地忽略空值和非法值,保持原值不变。这样可以避免因为一次异常输入破坏整个编辑会话。
五、调试与验证清单
完成集成后,建议按页面刷新、加载初始值、点击步进、手动输入、输入非法值、提交保存的路径逐步验证。每一阶段都有一个明确预期:刷新后Spinner显示实体当前值;点击后DataContext感知修改;手动输入后字段错误状态消失;非法值时字段显示错误信息且阻止提交;最终提交成功后数据库更新。通过这条路径可以快速定位问题出在客户端事件、服务端方法、ValueSource还是校验器。
如果遇到值显示正常但保存不生效,优先检查服务端字段的updateValueFromClient是否在客户端打包后仍被正确调用。浏览器控制台的网络或调试输出能帮助确认服务端方法是否收到值。如果校验不触发,则需要确认ValueChangeEvent的isFromClient标记是否为真,以及ValueSource的property字段是否与实体属性名一致。不要把所有错误都堆到校验阶段,先保证值到达实体,再补校验提示。
JmixjQuery UI Spinner数据绑定与校验修改时间:2026-09-20 17:09:19