Aurelia: validating form with reusable validatable

2019-07-10 01:44发布

问题:

Short question: How can I validate a parent form when the validation is part of child custom elements?

Long version:

I built a reusable custom element which includes validation which is working like I expect it to do:

validated-input.html:

<template>
<div class="form-group" validate.bind="validation">
    <label></label>
    <input type="text" value.bind="wert" class="form-control" />
</div>
</template>

validated-input.js:

import { bindable, inject } from 'aurelia-framework';
import { Validation } from 'aurelia-validation';

@inject(Validation)
export class ValidatedInputCustomElement {
    @bindable wert;

    constructor(validation) {
        this.validation = validation.on(this)
        .ensure('wert')
            .isNotEmpty()
            .isGreaterThan(0);
    }
} 

I will have some forms that will use this custom element more than once in the same view (can be up to 8 or 12 times or even more). A very simplified example could look like this:

<template>
  <require from="validated-input"></require>
  <form submit.delegate="submit()">
    <validated-input wert.two-way="val1"></validated-input>
    <validated-input wert.two-way="val2"></validated-input>
    <validated-input wert.two-way="val3"></validated-input>
    <button type="submit" class="btn btn-default">save</button>
  </form>
</template>

In the corresponding viewmodel file I would like to ensure that the data can only be submitted if everything is valid. I would like to do something like

this.validation.validate()
    .then(() => ...)
    .catch(() => ...);

but I don't understand yet how (or if) I can pull the overall validation into the parent view.

What I came up with up to now is referencing the viewmodel of my validated-input like this:

<validated-input wert.two-way="val1" view-model.ref="vi1"></validated-input>

and then to check it in the parent like this:

this.vi1.validation.validate()
    .then(() => ...)
    .catch(() => ...);

but this would make me need to call it 8/12/... times.

And I will probably have some additional validation included in the form.

Here is a plunkr with an example: https://plnkr.co/edit/v3h47GAJw62mlhz8DeLf?p=info

回答1:

You can define an array of validation objects (as Fabio Luz wrote) at the form level and then register the custom element validations in this array. The validation will be started on the form submit.

The form code looks like:

validationArray = [];

validate() {
    var validationResultsArray = [];

    // start the validation here
    this.validationArray.forEach(v => validationResultsArray.push(v.validate()));

    Promise.all(validationResultsArray)
        .then(() => this.resulttext = "validated")
        .catch(() => this.resulttext = "not validated");
}

validated-input.js gets a new function to register the validation

bind(context) {
    context.validationArray.push(this.validation);
}

The plunker example is here https://plnkr.co/edit/X5IpbwCBwDeNxxpn55GZ?p=preview



回答2:

but this would make me need to call it 8/12/... times.

And I will probably have some additional validation included in the form.

These lines are very important to me. In my opinion (considering that you do not want to call 8/12 times, and you also need an additional validation), you should validate the entire form, instead of each element. In that case, you could inject the validation in the root component (or the component that owns the form), like this:

import { Validation } from 'aurelia-validation';
import { bindable, inject } from 'aurelia-framework';

@inject(Validation)
export class App {
  val1 = 0;
  val2 = 1;
  val3 = 2;

  resulttext = "";

  constructor(validation) {
    this.validation = validation.on(this)
      .ensure('val1')
        .isNotEmpty()
        .isGreaterThan(0)
      .ensure('val2')
        .isNotEmpty()
        .isGreaterThan(0)
      .ensure('val3')
        .isNotEmpty()
        .isGreaterThan(0);
     //some additional validation here
  }

  validate() {
    this.validation.validate()
        .then(() => this.resulttext = "valid")
        .catch(() => this.resulttext = "not valid");
  }
}

View:

<template>
  <require from="validated-input"></require>
  <form submit.delegate="validate()" validation.bind="validation">
    <validated-input wert.two-way="val1"></validated-input>
    <validated-input wert.two-way="val2"></validated-input>
    <validated-input wert.two-way="val3"></validated-input>

    <button type="submit" class="btn btn-default">validate</button>
  </form>
  <div>${resulttext}</div>
</template>

Now, you can re-use the validated-input component in other places. And of course, you probably have to rename it, because the name validated-input does not make sense in this case.

Here is the plunker example https://plnkr.co/edit/gd9S2y?p=preview


Another approach would be pushing all the validation objects into an array and then calling a function to validate all validations objects, but that sounds strange to me.

Hope it helps!