feat: Form validation — rules-based field validation with inline error display

- FormBase: added validate_rules(), _check_rule(), show_field_error(), clear_all_errors()
- FieldGroup: creates hidden error Text widget for fields with rules
- validation(): calls validate_rules() before submit, blocks on failure
- CSS: .inputbox.field-error red border, .field-error-msg red text
- Supported rule types: required, minlength, maxlength, min, max, pattern, email, number
This commit is contained in:
Hermes Agent 2026-06-18 17:22:11 +08:00
parent e7ae56e142
commit 37f46ed1bd
2 changed files with 102 additions and 0 deletions

View File

@ -646,6 +646,12 @@ hr {
.inputbox:focus {
border-color: #007bff;
}
.inputbox.field-error {
border-color: #e74c3c;
}
.field-error-msg .bricks-text {
color: #e74c3c !important;
}
/* ========== Dark Theme (Sage Shell) ========== */
[data-theme="dark"] body,

View File

@ -57,6 +57,21 @@ bricks.FieldGroup = class {
box.add_widget(w);
form.name_inputs[fields[i].name] = w;
w.set_id(fields[i].name);
if (fields[i].rules && fields[i].rules.length > 0){
var errw = new bricks.Text({
otext:'',
height:'auto',
dynsize:true,
i18n:false,
cfontsize:0.8
});
errw.set_id(fields[i].name + '_error');
errw.set_css('field-error-msg');
errw.hide();
box.add_widget(errw);
form.name_error_widgets[fields[i].name] = errw;
form.field_rules[fields[i].name] = fields[i].rules;
}
} else {
bricks.debug(fields[i], 'createInput failed');
}
@ -136,6 +151,84 @@ bricks.FormBase = class extends bricks.Layout {
constructor(opts){
super(opts);
this.name_inputs = {};
this.name_error_widgets = {};
this.field_rules = {};
}
validate_rules(){
var valid = true;
this.clear_all_errors();
for (var name in this.field_rules){
if (!this.field_rules.hasOwnProperty(name)) continue;
var w = this.name_inputs[name];
if (!w) continue;
if (w.parent && w.parent.is_disabled && w.parent.is_disabled()) continue;
var d = w.getValue();
var value = d[name];
var rules = this.field_rules[name];
for (var i = 0; i < rules.length; i++){
var msg = this._check_rule(rules[i], value);
if (msg){
this.show_field_error(name, msg);
if (valid && w.focus) w.focus();
valid = false;
break;
}
}
}
return valid;
}
_check_rule(rule, value){
var v = (value === null || value === undefined) ? '' : String(value);
switch(rule.type){
case 'required':
if (v === '' || v.trim() === '') return rule.message;
break;
case 'minlength':
if (v !== '' && v.length < rule.value) return rule.message;
break;
case 'maxlength':
if (v.length > rule.value) return rule.message;
break;
case 'min':
if (v !== ''){
var n = parseFloat(v);
if (isNaN(n) || n < rule.value) return rule.message;
}
break;
case 'max':
if (v !== ''){
var n = parseFloat(v);
if (isNaN(n) || n > rule.value) return rule.message;
}
break;
case 'pattern':
if (v !== '' && !new RegExp(rule.value).test(v)) return rule.message;
break;
case 'email':
if (v !== '' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) return rule.message;
break;
case 'number':
if (v !== '' && isNaN(parseFloat(v))) return rule.message;
break;
}
return null;
}
show_field_error(name, msg){
var ew = this.name_error_widgets[name];
if (ew){
ew.set_otext(msg);
ew.show();
}
var box = bricks.getWidgetById(name + '_box', this);
if (box && box.dom_element) box.dom_element.classList.add('field-error');
}
clear_all_errors(){
for (var name in this.name_error_widgets){
var ew = this.name_error_widgets[name];
if (ew){ ew.set_otext(''); ew.hide(); }
var box = bricks.getWidgetById(name + '_box', this);
if (box && box.dom_element) box.dom_element.classList.remove('field-error');
}
}
build_toolbar(widget){
var box = new bricks.HBox({height:'auto', width:'100%'});
@ -308,6 +401,9 @@ bricks.FormBase = class extends bricks.Layout {
return null
}
async validation(){
if (!this.validate_rules()){
return;
}
var running = new bricks.Running({target:this});
try {
var data;