Search code examples
decoratorecmascript-6aurelia

How to use aurelia-validate with a object properties to validate?


I'm using aurelia-validate and my validation works fine if I use variables, but I need it to validate properties of an object rather than a variable:

Here's what works:

  import {Validation} from 'aurelia-validation';
  import {ensure} from 'aurelia-validation';
  import {ItemService} from './service';

  export class EditItem {
    static inject() {
      return [Validation, ItemService];
    }

    @ensure(function(it){
      it.isNotEmpty()
        .hasLengthBetween(3,10);
    })
      name = '';

    @ensure(function(it){
      it.isNotEmpty()
        .hasMinLength(10)
        .matches(/^https?:\/\/.{3,}$/) //looks like a url
        .matches(/^\S*$/); //no spaces
    })
      url = '';

    constructor(validation, service) {
      this.validation = validation.on(this);
      this.service = service;
    }

    activate(params){
      return this.service.getItem(params.id).then(res => {
        console.log(res);
        this.name = res.content.name; //populate
        this.url = res.content.url;
      });
    }

    update() {
      this.validation.validate().then(
        () => {
          var data = {
            name: this.name,
            url: this.url
          };

          this.service.updateItem(data).then(res => {
            this.message = "Thank you!";
          })
        }
      );
    }
  }

Here's what I'm trying to do (but doesn't work)...also I'm not sure if it's better to keep the properties on the class or have a property called this.item which contains the properties (this is the typical angular way):

  import {Validation} from 'aurelia-validation';
  import {ensure} from 'aurelia-validation';
  import {ItemService} from './service';

  export class EditItem {
    static inject() {
      return [Validation, ItemService];
    }

    @ensure(function(it){
      it.isNotEmpty()
        .hasLengthBetween(3,10);
    })
      this.item.name; //no assignment here should happen 

    @ensure(function(it){
      it.isNotEmpty()
        .hasMinLength(10)
        .matches(/^https?:\/\/.{3,}$/) //looks like a url
        .matches(/^\S*$/); //no spaces
    })
      this.item.url; //no assignment?

    constructor(validation, service) {
      this.validation = validation.on(this);
      this.service = service;
      this.item = null;
    }

    activate(params){
      return this.service.getItem(params.id).then(res => {
        console.log(res);
        this.item = res.content; //populate with object from api call
      });
    }

    update() {
      this.validation.validate().then(
        () => {
          var data = {
            name: this.item.name,
            url: this.item.url
          };

          this.service.updateItem(data).then(res => {
            this.message = "Thank you!";
          })
        }
      );
    }
  }

Can someone give me some guidance here on how to use a validator against an existing object (for an edit page)?


Solution

  • The validation works in all kinds of situations, but using the @ensure decorator can only be used to declare your rules on simple properties (like you found out).

    Hence...

    Option a: replace the ensure decorator with the fluent API 'ensure' method, this supports 'nested' or 'complex' binding paths such as:

    import {Validation} from 'aurelia-validation';
    import {ItemService} from './service';
    
    export class EditItem {
    static inject() {
      return [Validation, ItemService];
    }
    
    constructor(validation, service) {
      this.validation = validation.on(this)
        .ensure('item.url')
          .isNotEmpty()
          .hasMinLength(10)
          .matches(/^https?:\/\/.{3,}$/) //looks like a url
          .matches(/^\S*$/)
        .ensure('item.name')
          .isNotEmpty()
          .hasLengthBetween(3,10);
      this.service = service;
      this.item = null;
    }
    
    activate(params){
      return this.service.getItem(params.id).then(res => {
        console.log(res);
        this.item = res.content; //populate with object from api call
      });
    }
    
    update() {
      this.validation.validate().then(
        () => {
          var data = {
            name: this.item.name,
            url: this.item.url
          };
    
          this.service.updateItem(data).then(res => {
            this.message = "Thank you!";
          })
        }
      );
    }
    

    }

    Note: you can set up your validation even before item is set. Cool, no?

    Option b: Since the validation rules are specific to the item, you could move your validation rules inside your item class using the @ensure decorator inside that class instead. You can then set up validation in your VM after you've retrieved the item: this.validation = validation.on(this.item); or, your service can set up the validation when it returns your item to your VM and make it an intrinsic part of the model: item.validation = validation.on(item);

    Option a is easiest and seems to match your experience. Option b is more maintainable, as the validation rules for your model will live on the model, not on the view-model. However if you go with option b, you might have to adjust your HTML a bit to make sure validation hints appear.