foreach transformer
foreach
is a feature that sets bcx-validation
apart from other alternatives. It made easy to validate dynamic models.
The get-started showed an example of foreach
transformer. It uses item index as the error key, that’s default behaviour of foreach
transformer.
Error key override
Let’s use customer id as the error key, this makes it easier to inspect error object.
var rule = {
customers: {
foreach: {
name: ['mandatory', 'unique'],
age: ['notMandatory', {validate: 'number', min: 16}],
id: ['mandatory', 'unique']
},
key: 'id'
}
};
var model = {
customers: [
{id: 'aa', name: 'Arm'},
{id: 'ab', name: 'Bob'},
{id: 'ab', name: 'Bob', age: 15},
{id: 'ad', name: '', age: 18}
]
};
validation.validate(model, rule);
/* =>
{
customers: {
ab: {
name: ['must be unique'],
age: ['must be at least 16'],
id: ['must be unique']
},
ad: {
name: ['must not be empty']
}
}
}
*/
Note since the id “ab” is not unique, the error only appears once. If you don’t use
key: "id"
error key override, the error will appears twice, one on “1”, another on “2”.
key can use either expression or function.
Special context variables introduced by foreach
Underneath, for every item in the model array, foreach
transformer creates new contextual proxy and add few special context variables.
Similar to what aurelia repeater does in html template.
For example, when foreach
is validating property “name” of the first item {id: 'aa', name: 'Arm'}
, it has following context variables.
{
$value: 'Arm'
$propertyPath: ['name']
$this: {id: 'aa', name: 'Arm'}
$parent: {customers: [ /* all 4 */ ]}
// introduced by foreach
$neighbours: [{id: 'ab', name: 'Bob'}, {id: 'ab', name: 'Bob', age: 15}, {id: 'ad', name: '', age: 18}]
$neighbourValues: ['Bob', 'Bob', '']
$index: 0
$first: true
$last: false
}
$propertyPath
is an array of property names, it can have multiple items (like['address', 'line1']
) if it’s a deep nested validation. Internally, we use it as lodash property path. You may wonder why we pick the complex array form['address', 'line1']
, not the simple dot notation form'address.line1'
. Lodash supports both forms, but for some property name like “x.y”, dot notation doesn’t work (a.x.y
does not meana["x.y"]
).
$this
is the new context created by foreach, which is the current item.
$parent
is the outer/parent context.
$neighbours
are all the siblings, BUT DO NOT INCLUDE $this itself.
$neighbourValues
are the relevant property values on $neighbours, they are same as_.map($neighbours, _.property($propertyPath))
when $propertyPath is not empty. When $propertyPath is empty (explained later in “Use foreach to validate simple array”), $neighbourValues are same as $neighbours.
$index
,$first
,$last
are for the position of current item. They are similar to what aurelia repeater provides.
For people with aurelia experience, note we don’t have $even, $odd context variables in foreach. We don’t provide them to avoid conflict with our standard “number” validator which supports $even/$odd options.
Let’s have a look how we defined standard “unique” validator.
// copied from standard-validators.js
// unique. need to access neighbours
// option items is evaluated from current scope
validation.addValidator('unique', {validate: 'notIn', 'items.bind': '$neighbourValues', message: 'must be unique'});
“unique” validator reuses “notIn” validator, and use $neighbourValues in “notIn”’s “items” option.
This is a good example of using “option.bind”. In validator composition, use “bind” to pass runtime information to underneath validators.
Let’s do another exercise around foreach context variables. Let’s validate, that in a group of people, there are only certain maximum number of leaders.
validation.addValidator('maxLeader', {
if: '$this.leader',
validate: 'number',
value: '_($neighbours).filter({leader: true}).size()',
'max.bind': '$max - 1',
message: 'Only maximum ${$max} leaders allowed'
});
We reuse “number” validator, when current person is a leader, checks the count of all neighbour leaders, the number can not exceed (max - 1).
Note that we didn’t check $max option, it assumes the option is mandatory. For safety, you can use
if: "$this.leader && $max > 0"
.
Note we didn’t use $value or $propertyPath in any of the expressions. This means our “maxLeader” validator can be applied to any property of a person. We will elaborate this in examples.
We passed ($max - 1) to underneath “number” validator’s “max” option. This is not the only way to write “maxLeader”, you can also do
{validate: "isTrue", value: "_($neighbours).filter({leader: true}).size() <= ($max - 1)"}
.
Let’s try out our “maxLeader” validator.
var validate2Leaders = validation.generateValidator({
foreach: {
name: ['mandatory', 'unique'],
leader: {validate: 'maxLeader', max: 2}
}
});
validate2Leaders([
{name: 'A', leader: true},
{name: 'B', leader: true},
{name: 'C', leader: true},
{name: 'D'},
]);
/* =>
{ '0': { leader: [ 'Only maximum 2 leaders allowed' ] },
'1': { leader: [ 'Only maximum 2 leaders allowed' ] },
'2': { leader: [ 'Only maximum 2 leaders allowed' ] } }
*/
validate2Leaders([
{name: 'A', leader: true},
{name: 'B', leader: true},
{name: 'C'},
{name: 'D'},
]);
// => undefined
Because the way we design “maxLeader”, we can use it on other property instead of “leader” property.
var validate2LeadersOnNameProperty = validation.generateValidator({
foreach: {
name: ['mandatory', 'unique', {validate: 'maxLeader', max: 2}],
}
});
validate2LeadersOnNameProperty([
{name: 'A', leader: true},
{name: 'B', leader: true},
{name: 'C', leader: true},
{name: 'D'},
]);
/* =>
{ '0': { name: [ 'Only maximum 2 leaders allowed' ] },
'1': { name: [ 'Only maximum 2 leaders allowed' ] },
'2': { name: [ 'Only maximum 2 leaders allowed' ] } }
*/
Let’s show you another version of “maxLeader”. This time, assume it’s validating “leader” property.
validation.addValidator('maxLeader', {
if: '$value', // current item.leader value
validate: 'number',
value: '_($neighbourValues).compact().size()', // _.compact removes false leader
'max.bind': '$max - 1',
message: 'Cannot exceed maximum ${$max} leaders'
});
validate2Leaders([
{name: 'A', leader: true},
{name: 'B', leader: true},
{name: 'C', leader: true},
{name: 'D'},
]);
/* => validating on 'leader' still works
{ '0': { leader: [ 'Cannot exceed maximum 2 leaders' ] },
'1': { leader: [ 'Cannot exceed maximum 2 leaders' ] },
'2': { leader: [ 'Cannot exceed maximum 2 leaders' ] } }
*/
validate2LeadersOnNameProperty([
{name: 'A', leader: true},
{name: 'B', leader: true},
{name: 'C', leader: true},
{name: 'D'},
]);
/* => validating on 'name', oops, all names are truthy
{ '0': { name: [ 'Cannot exceed maximum 2 leaders' ] },
'1': { name: [ 'Cannot exceed maximum 2 leaders' ] },
'2': { name: [ 'Cannot exceed maximum 2 leaders' ] },
'3': { name: [ 'Cannot exceed maximum 2 leaders' ] } }
*/
An example to show foreach
and switch
work nicely together.
var rule = {
users: {
foreach: {
switch: 'type',
cases: {
customer: {
email: ['notMandatory', 'email'],
phone: ['notMandatory', 'unique'],
name: ['mandatory', 'unique']
},
dealer: {
dealerId: ['mandatory', 'unique'],
phone: ['mandatory', 'unique'],
email: ['mandatory', 'email'],
name: ['mandatory', 'unique']
}
}
},
key: 'id'
}
};
var model = {
users: [
{id: 'c01', type: 'customer', name: 'Arm', email: 'arm@test.com'},
{id: 'c02', type: 'customer', name: 'Bob', email: 'bob@test.com'},
{id: 'c03', type: 'customer', name: 'Bob', email: 'bob'},
{id: 'd01', type: 'dealer', name: 'Dealer A', email: 'arm@test.com'},
{id: 'd02', dealerId: 'dealer.b', type: 'dealer', name: 'Dealer B', email: 'on', phone: '02123'},
{id: 'd03', dealerId: 'dealer.b', type: 'dealer', name: 'Dealer B', email: 'b@test.com', phone: '02123'},
]
};
validation.validate(model, rule);
/* =>
{ 'users': {
'c02': { 'name': ['must be unique'] },
'c03': { 'email': ['not a valid email'],
'name': ['must be unique'] },
'd01': { 'dealerId': ['must not be empty'],
'phone': ['must not be empty'] },
'd02': { 'dealerId': ['must be unique'],
'phone': ['must be unique'],
'email': ['not a valid email'],
'name': ['must be unique'] },
'd03': { 'dealerId': ['must be unique'],
'phone': ['must be unique'],
'name': ['must be unique'] } } }
*/
Chain rule works under foreach
too. Following rule works same as previous one.
var rule = {
users: {
foreach: [
// generic rule on every customer/dealer
{
name: ['mandatory', 'unique'],
email: ['notMandatory', 'email'],
phone: ['notMandatory', 'unique']
},
// strict rule on dealer
{
switch: 'type',
cases: {
dealer: {
dealerId: ['mandatory', 'unique'],
phone: ['mandatory', 'unique'],
email: ['mandatory', 'email'],
}
}
}
],
key: 'id'
}
};
Note we use rule for “customer” as the first one in chain. Second one only validating “dealer”. Since all rules in “dealer” are stricter, the final result generated by
bcx-validation
will be nicely merged.
The order of the rules doesn’t matter here. If you swap the position of generic rule and strict rule, the final result is still same.
Use foreach to validate simple array
You can use foreach
to validate simple array (not array of complex object).
validation.validate(['xx', 'ab@test.com', '-xi@ a'], {foreach: 'email'});
// => { '0': [ 'not a valid email' ], '2': [ 'not a valid email' ] }
Use foreach to validate object
foreach
is designed to validate array, but you still can use it to validate a normal object. Obviously $index, $first, $last do not make sense in this use case.
validation.validate({meta: {field1: ' ', field2: 'hello'}},
{meta: {foreach: 'mandatory'}});
// => { meta: { field1: [ 'must not be empty' ] } }
foreach
with rule factory function
The last feature of foreach
is that it treats raw function specially.
We learnt before that a raw function is treated as raw validator implementation. But if it’s used under foreach
, it is treated as rule factory.
This only applies to top level raw function, either in
{foreach: aRuleFactoryFunc}
or{foreach: [normalRule, aRuleFactoryFunc, anotherRuleFactoryFunc,...]}
.
This is designed to provide flexibility in
foreach
whenswitch
andif
is not enough for conditional validation.
There is trade-off for this flexibility. Because rule factory requires runtime information to build the rule, it cannot be pre-compiled. This means you would not see much performance benefit with
generateValidator
.
The above foreach
+ switch
example can be rewritten as:
var rule = {
users: {
foreach: (user) => {
switch(user.type) {
case 'customer':
return {
email: ['notMandatory', 'email'],
phone: ['notMandatory', 'unique'],
name: ['mandatory', 'unique']
};
case 'dealer':
return {
dealerId: ['mandatory', 'unique'],
phone: ['mandatory', 'unique'],
email: ['mandatory', 'email'],
name: ['mandatory', 'unique']
};
}
},
key: 'id'
}
};
Let’s move on to add helper.