Skip to content

Commit ed4a3fa

Browse files
committed
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 <newtwen+github@gmail.com>
1 parent 04b289c commit ed4a3fa

File tree

3 files changed

+67
-20
lines changed

3 files changed

+67
-20
lines changed

modules/luci-base/htdocs/luci-static/resources/form.js

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,6 +1920,37 @@ const CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
19201920
return true;
19211921
},
19221922

1923+
/**
1924+
* Get the validator function for the widget, handling both single functions
1925+
* and arrays of functions.
1926+
*
1927+
* @private
1928+
* @param {string} section_id
1929+
* The configuration section ID
1930+
*
1931+
* @returns {function}
1932+
* Returns a bound validator function suitable for passing to UI widgets.
1933+
* If this.validate is an array, returns a wrapper that calls each validator
1934+
* serially. Otherwise returns the bound validate method.
1935+
*/
1936+
getValidator(section_id) {
1937+
if (Array.isArray(this.validate)) {
1938+
const validators = this.validate;
1939+
const element = this;
1940+
return (value) => {
1941+
for (let val of validators) {
1942+
if (typeof(val) === 'function') {
1943+
const result = val.call(element, section_id, value);
1944+
if (result !== true)
1945+
return result;
1946+
}
1947+
}
1948+
return true;
1949+
};
1950+
}
1951+
return L.bind(this.validate, this, section_id);
1952+
},
1953+
19231954
/**
19241955
* Test whether the input value is currently valid.
19251956
*
@@ -3870,7 +3901,7 @@ const CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */
38703901
optional: this.optional || this.rmempty,
38713902
datatype: this.datatype,
38723903
select_placeholder: this.placeholder ?? placeholder,
3873-
validate: L.bind(this.validate, this, section_id),
3904+
validate: this.getValidator(section_id),
38743905
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
38753906
});
38763907
}
@@ -3881,7 +3912,7 @@ const CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */
38813912
optional: this.optional || this.rmempty,
38823913
datatype: this.datatype,
38833914
placeholder: this.placeholder,
3884-
validate: L.bind(this.validate, this, section_id),
3915+
validate: this.getValidator(section_id),
38853916
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
38863917
});
38873918
}
@@ -3949,7 +3980,7 @@ const CBIDynamicList = CBIValue.extend(/** @lends LuCI.form.DynamicList.prototyp
39493980
optional: this.optional || this.rmempty,
39503981
datatype: this.datatype,
39513982
placeholder: this.placeholder,
3952-
validate: L.bind(this.validate, this, section_id),
3983+
validate: this.getValidator(section_id),
39533984
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
39543985
});
39553986

@@ -4041,7 +4072,7 @@ const CBIListValue = CBIValue.extend(/** @lends LuCI.form.ListValue.prototype */
40414072
optional: this.optional,
40424073
orientation: this.orientation,
40434074
placeholder: this.placeholder,
4044-
validate: L.bind(this.validate, this, section_id),
4075+
validate: this.getValidator(section_id),
40454076
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
40464077
});
40474078

@@ -4136,7 +4167,7 @@ const CBIRichListValue = CBIListValue.extend(/** @lends LuCI.form.ListValue.prot
41364167
orientation: this.orientation,
41374168
select_placeholder: this.select_placeholder || this.placeholder,
41384169
custom_placeholder: this.custom_placeholder || this.placeholder,
4139-
validate: L.bind(this.validate, this, section_id),
4170+
validate: this.getValidator(section_id),
41404171
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
41414172
});
41424173

@@ -4281,7 +4312,7 @@ const CBIRangeSliderValue = CBIValue.extend(/** @lends LuCI.form.RangeSliderValu
42814312
calcunits: this.calcunits,
42824313
disabled: this.readonly || this.disabled,
42834314
datatype: this.datatype,
4284-
validate: L.bind(this.validate, this, section_id),
4315+
validate: this.getValidator(section_id),
42854316
});
42864317

42874318
this.widget = slider;
@@ -4403,7 +4434,7 @@ const CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.Flag.prototype */ {
44034434
id: this.cbid(section_id),
44044435
value_enabled: this.enabled,
44054436
value_disabled: this.disabled,
4406-
validate: L.bind(this.validate, this, section_id),
4437+
validate: this.getValidator(section_id),
44074438
tooltip,
44084439
tooltipicon: this.tooltipicon,
44094440
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
@@ -4546,7 +4577,7 @@ const CBIMultiValue = CBIDynamicList.extend(/** @lends LuCI.form.MultiValue.prot
45464577
create: this.create,
45474578
display_items: this.display_size ?? this.size ?? 3,
45484579
dropdown_items: this.dropdown_size ?? this.size ?? -1,
4549-
validate: L.bind(this.validate, this, section_id),
4580+
validate: this.getValidator(section_id),
45504581
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
45514582
});
45524583

@@ -4639,7 +4670,7 @@ const CBITextValue = CBIValue.extend(/** @lends LuCI.form.TextValue.prototype */
46394670
cols: this.cols,
46404671
rows: this.rows,
46414672
wrap: this.wrap,
4642-
validate: L.bind(this.validate, this, section_id),
4673+
validate: this.getValidator(section_id),
46434674
readonly: (this.readonly != null) ? this.readonly : this.map.readonly,
46444675
disabled: (this.disabled != null) ? this.disabled : null,
46454676
});

modules/luci-base/htdocs/luci-static/resources/ui.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,14 @@ const UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype
5555
* It defaults to `string` which will allow any value.
5656
* See {@link LuCI.validation} for details on the expression format.
5757
*
58-
* @property {function} [validator]
59-
* Specifies a custom validator function which is invoked after the
60-
* standard validation constraints are checked. The function should return
61-
* `true` to accept the given input value. Any other return value type is
62-
* converted to a string and treated as validation error message.
58+
* @property {function|function[]} [validator]
59+
* Specifies one or more custom validator functions which are invoked after
60+
* the standard validation constraints are checked. Each function should
61+
* return `true` to accept the given input value. When multiple functions
62+
* are provided as an array, they are executed serially and validation stops
63+
* at the first function that returns a non-true value. Any non-true return
64+
* value type is converted to a string and treated as a validation error
65+
* message.
6366
*
6467
* @property {boolean} [disabled=false]
6568
* Specifies whether the widget should be rendered in disabled state
@@ -5266,11 +5269,14 @@ const UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
52665269
* If an input element is not marked optional it must not be empty,
52675270
* otherwise it will be marked as invalid.
52685271
*
5269-
* @param {function} [vfunc]
5270-
* Specifies a custom validation function which is invoked after the
5271-
* other validation constraints are applied. The validation must return
5272-
* `true` to accept the passed value. Any other return type is converted
5273-
* to a string and treated as validation error message.
5272+
* @param {function|function[]} [vfunc]
5273+
* Specifies a custom validation function or an array of validation functions
5274+
* which are invoked after the other validation constraints are applied. Each
5275+
* function must return `true` to accept the passed value. When multiple
5276+
* functions are provided as an array, they are executed serially and
5277+
* validation stops at the first function that returns a non-true value.
5278+
* Any non-true return type is converted to a string and treated as validation
5279+
* error message.
52745280
*
52755281
* @param {...string} [events=blur, keyup]
52765282
* The list of events to bind. Each received event will trigger a field

modules/luci-base/htdocs/luci-static/resources/validation.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,18 @@ const Validator = baseclass.extend({
8686
return false;
8787
}
8888

89-
if (typeof(this.vfunc) == 'function')
89+
if (typeof(this.vfunc) == 'function') {
9090
valid = this.vfunc(this.value);
91+
} else if (Array.isArray(this.vfunc)) {
92+
/* Execute validation functions serially */
93+
for (let val of this.vfunc) {
94+
if (typeof(val) == 'function') {
95+
valid = val(this.value);
96+
if (valid !== true)
97+
break;
98+
}
99+
}
100+
}
91101

92102
if (valid !== true) {
93103
this.assert(false, valid);

0 commit comments

Comments
 (0)