From: Paul Donald Date: Fri, 23 Jan 2026 19:53:21 +0000 (+0100) Subject: luci-base: add validator array support X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=ed4a3fa933954bb08eaed7a259c6eaa9eb22aa11;p=openwrt-luci.git luci-base: add validator array support While it's possible to stack validation using .datatype semantics, e.g. o.datatype = 'and(foo,bar)' and those execute serially, they depend on the internal validation factory and the built-ins there. Now, one can define multiple functions in the calling code (passed in an array) which execute serially. All validation functions shall return true to succeed. e.g. ```js function foo(sid, val) { return val.includes('foo'); } function bar(sid, val) { return val.includes('bar'); } o = s.option(form.Value, 'foobar', _('foobar')); o.default = 'foobar'; o.validate = [foo, bar]; ``` This helps make validation less complex when special data-types with high cardinality are in play. Previously, reuse of the this context when calling sub functions was also lost. The validate property passed to the new ui widget in form.js ```js new ui.XX(..., { ... validate: this.getValidator(section_id), ...}); ``` takes this.getValidator, which either binds a single validation function or calls all of the validators, if the form element's this.validate was an [array]. The result and effect are the same. If a ui element is manually instantiated (when it's not being called via form.js and this.getValidator might be unavailable), passing an array to options.validate works identically: ```js function foo(val) { return val.includes('foo'); } function bar(val) { return val.includes('bar'); } new ui.XX(..., { ... validate: [foo, bar], ...}); ``` And the validation factory calls them serially. Signed-off-by: Paul Donald --- diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js index de6c12290d..8d627f8f07 100644 --- a/modules/luci-base/htdocs/luci-static/resources/form.js +++ b/modules/luci-base/htdocs/luci-static/resources/form.js @@ -1920,6 +1920,37 @@ const CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract return true; }, + /** + * Get the validator function for the widget, handling both single functions + * and arrays of functions. + * + * @private + * @param {string} section_id + * The configuration section ID + * + * @returns {function} + * Returns a bound validator function suitable for passing to UI widgets. + * If this.validate is an array, returns a wrapper that calls each validator + * serially. Otherwise returns the bound validate method. + */ + getValidator(section_id) { + if (Array.isArray(this.validate)) { + const validators = this.validate; + const element = this; + return (value) => { + for (let val of validators) { + if (typeof(val) === 'function') { + const result = val.call(element, section_id, value); + if (result !== true) + return result; + } + } + return true; + }; + } + return L.bind(this.validate, this, section_id); + }, + /** * Test whether the input value is currently valid. * @@ -3870,7 +3901,7 @@ const CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ optional: this.optional || this.rmempty, datatype: this.datatype, select_placeholder: this.placeholder ?? placeholder, - validate: L.bind(this.validate, this, section_id), + validate: this.getValidator(section_id), disabled: (this.readonly != null) ? this.readonly : this.map.readonly }); } @@ -3881,7 +3912,7 @@ const CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ optional: this.optional || this.rmempty, datatype: this.datatype, placeholder: this.placeholder, - validate: L.bind(this.validate, this, section_id), + validate: this.getValidator(section_id), disabled: (this.readonly != null) ? this.readonly : this.map.readonly }); } @@ -3949,7 +3980,7 @@ const CBIDynamicList = CBIValue.extend(/** @lends LuCI.form.DynamicList.prototyp optional: this.optional || this.rmempty, datatype: this.datatype, placeholder: this.placeholder, - validate: L.bind(this.validate, this, section_id), + validate: this.getValidator(section_id), disabled: (this.readonly != null) ? this.readonly : this.map.readonly }); @@ -4041,7 +4072,7 @@ const CBIListValue = CBIValue.extend(/** @lends LuCI.form.ListValue.prototype */ optional: this.optional, orientation: this.orientation, placeholder: this.placeholder, - validate: L.bind(this.validate, this, section_id), + validate: this.getValidator(section_id), disabled: (this.readonly != null) ? this.readonly : this.map.readonly }); @@ -4136,7 +4167,7 @@ const CBIRichListValue = CBIListValue.extend(/** @lends LuCI.form.ListValue.prot orientation: this.orientation, select_placeholder: this.select_placeholder || this.placeholder, custom_placeholder: this.custom_placeholder || this.placeholder, - validate: L.bind(this.validate, this, section_id), + validate: this.getValidator(section_id), disabled: (this.readonly != null) ? this.readonly : this.map.readonly }); @@ -4281,7 +4312,7 @@ const CBIRangeSliderValue = CBIValue.extend(/** @lends LuCI.form.RangeSliderValu calcunits: this.calcunits, disabled: this.readonly || this.disabled, datatype: this.datatype, - validate: L.bind(this.validate, this, section_id), + validate: this.getValidator(section_id), }); this.widget = slider; @@ -4403,7 +4434,7 @@ const CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.Flag.prototype */ { id: this.cbid(section_id), value_enabled: this.enabled, value_disabled: this.disabled, - validate: L.bind(this.validate, this, section_id), + validate: this.getValidator(section_id), tooltip, tooltipicon: this.tooltipicon, disabled: (this.readonly != null) ? this.readonly : this.map.readonly @@ -4546,7 +4577,7 @@ const CBIMultiValue = CBIDynamicList.extend(/** @lends LuCI.form.MultiValue.prot create: this.create, display_items: this.display_size ?? this.size ?? 3, dropdown_items: this.dropdown_size ?? this.size ?? -1, - validate: L.bind(this.validate, this, section_id), + validate: this.getValidator(section_id), disabled: (this.readonly != null) ? this.readonly : this.map.readonly }); @@ -4639,7 +4670,7 @@ const CBITextValue = CBIValue.extend(/** @lends LuCI.form.TextValue.prototype */ cols: this.cols, rows: this.rows, wrap: this.wrap, - validate: L.bind(this.validate, this, section_id), + validate: this.getValidator(section_id), readonly: (this.readonly != null) ? this.readonly : this.map.readonly, disabled: (this.disabled != null) ? this.disabled : null, }); diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js index c8857b01c5..b9ccb1e187 100644 --- a/modules/luci-base/htdocs/luci-static/resources/ui.js +++ b/modules/luci-base/htdocs/luci-static/resources/ui.js @@ -55,11 +55,14 @@ const UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype * It defaults to `string` which will allow any value. * See {@link LuCI.validation} for details on the expression format. * - * @property {function} [validator] - * Specifies a custom validator function which is invoked after the - * standard validation constraints are checked. The function should return - * `true` to accept the given input value. Any other return value type is - * converted to a string and treated as validation error message. + * @property {function|function[]} [validator] + * Specifies one or more custom validator functions which are invoked after + * the standard validation constraints are checked. Each function should + * return `true` to accept the given input value. When multiple functions + * are provided as an array, they are executed serially and validation stops + * at the first function that returns a non-true value. Any non-true return + * value type is converted to a string and treated as a validation error + * message. * * @property {boolean} [disabled=false] * Specifies whether the widget should be rendered in disabled state @@ -5266,11 +5269,14 @@ const UI = baseclass.extend(/** @lends LuCI.ui.prototype */ { * If an input element is not marked optional it must not be empty, * otherwise it will be marked as invalid. * - * @param {function} [vfunc] - * Specifies a custom validation function which is invoked after the - * other validation constraints are applied. The validation must return - * `true` to accept the passed value. Any other return type is converted - * to a string and treated as validation error message. + * @param {function|function[]} [vfunc] + * Specifies a custom validation function or an array of validation functions + * which are invoked after the other validation constraints are applied. Each + * function must return `true` to accept the passed value. When multiple + * functions are provided as an array, they are executed serially and + * validation stops at the first function that returns a non-true value. + * Any non-true return type is converted to a string and treated as validation + * error message. * * @param {...string} [events=blur, keyup] * The list of events to bind. Each received event will trigger a field diff --git a/modules/luci-base/htdocs/luci-static/resources/validation.js b/modules/luci-base/htdocs/luci-static/resources/validation.js index 5959338bf4..9e44e47185 100644 --- a/modules/luci-base/htdocs/luci-static/resources/validation.js +++ b/modules/luci-base/htdocs/luci-static/resources/validation.js @@ -86,8 +86,18 @@ const Validator = baseclass.extend({ return false; } - if (typeof(this.vfunc) == 'function') + if (typeof(this.vfunc) == 'function') { valid = this.vfunc(this.value); + } else if (Array.isArray(this.vfunc)) { + /* Execute validation functions serially */ + for (let val of this.vfunc) { + if (typeof(val) == 'function') { + valid = val(this.value); + if (valid !== true) + break; + } + } + } if (valid !== true) { this.assert(false, valid);