In my experience there are two ways of doing this. In both ways, view the array contents as an object on its own (i.e. the url object), which is validated separately to the parent object which contains the array (which could have other properties or arrays too).
For example, lets say you have this basic structure:
class Url {
protocol: string;
host: string;
port: number;
}
class Foo {
urls: Url[];
title: string;
}
The first method (for simple objects and views) would be to add each url object to the validation controller manually. The advantage of this method is simplicity of files in your project, and everything is in one viewmodel. The disadvantage of this method, is that every time you mutate the array, you need to add or remove the objects from the validation controller as well. (Also when you enter/leave the page, or after you’ve loaded/reloaded data from the server). As you can see that can be a lot to remember.
bind() {
ValidationRules
.ensure((f: Foo) => f.title).required()
.ensure(f => f.urls).required().minItems(1) // validates how many urls are required
.on(this.foo);
this.urlRules = ValidationRules
.ensure((u: Url) => u.protocol).required()
.ensure(u => u.host).required()
.ensure(u => u.port).required().satisfies((val, obj) => {
if (val == null) { return true; }
return val > 0;
})
.rules; // get a ruleset we can apply to objects later
this.foo.urls.forEach(url => {
this.controller.addObject(url, this.urlRules);
});
}
unbind() {
this.foo.urls.forEach(url => {
this.controller.removeObject(url);
});
}
And of course don’t forget to validate the bound objects in your html:
<template>
<div repeat.for="url of foo.urls">
<input value.bind="url.protocol & validate">
<input value.bind="url.host & validate">
<input value.bind="url.port & validate">
</div>
</template>
The second method is to let Aurelia do the heavy work of adding/removing objects to validate. Achieve this by creating a custom element, which is validated in its own view model. The advantage is less code, and you don’t have to remember when to add or remove an object from validation, because the custom element instances will do that for themselves.
Your “parent” view model simply validates its own properties. Note how there’s no addObject or removeObject:
bind() {
ValidationRules
.ensure((f: Foo) => f.title).required()
.ensure(f => f.urls).required().minItems(1) // validates how many urls are required
.on(this.foo);
}
And your url view model handles itself:
@autoinject
export class UrlViewModel {
@bindable({ defaultBindingMode: bindingMode.twoWay }) url: Url;
constructor(private readonly controller: ValidationController) {
}
bind() {
ValidationRules
.ensure((u: Url) => u.protocol).required()
.ensure(u => u.host).required()
.ensure(u => u.port).required().satisfies((val, obj) => {
if (val == null) { return true; }
return val > 0;
})
.on(this.url);
}
}
Your parent view simply declares an array of the custom elements:
<template>
<require from="./url"></require>
<url repeat.for="url of urls" url.bind="url"></url>
</template>
I created a gist here, but I haven’t tested it so let me know if it breaks! https://gist.github.com/thinkOfaNumber/95709d027ed95182baa4e4411b54a5d0
HTH