Conditional Validations with ember-changeset-validations

Last reviewed on February 11, 2018

I was recently working on a feature that required lots of conditional validation. I used ember-changeset and ember-changeset-validations, as I love how you can buffer changes on your model and declaratively specify validation rules.

As I was looking through ember-changeset-validations, it didn't seem like the addon addressed conditional validation. I wanted to run validation checks against a field only if another field was present.

One way I could achieve this was to write my own validator for each field that required conditional validation. However, this wasn't as declarative as I'd like, and I'd have to repeat this for every field that needed conditional validation.

I turned to Laravel's Validator class for complex conditional validation for some inspiration, as I've always appreciated how intuitive the APIs are in Laravel. If you haven't used Laravel before, and the Validator class in particular, here is a simple example:

$validator = Validator::make($data, [
  'paymentMethod' => 'required'
]);

$validator->sometimes('creditCardNumber', 'required|size:16', function ($input) {
  return $input->paymentMethod === PaymentMethod::CREDIT_CARD;
});

So here is what I came up with for ember-changeset:

import { get } from '@ember/object';
import {
  validatePresence,
  validateLength,
} from 'ember-changeset-validations/validators';
import validateSometimes from 'ember-changeset-conditional-validations/validators/sometimes';

export default {
  creditCardNumber: validateSometimes(
    [validatePresence(true), validateLength({ is: 16 })],
    function (changes, content) {
      return (
        get(changes, 'paymentMethod.isCreditCard') ||
        get(content, 'paymentMethod.isCreditCard')
      );
    }
  ),
};

The way this works is that you can specify any number of validation rules for a property, and they will only be applied if the callback function to validateSometimes() returns true. In this example, paymentMethod would be some kind of model with a computed property isCreditCard. The callback function is invoked with the changeset's changes and the original content object.

This works, but I didn't like having to check both the changes and the content object for the same property, so I added a this.get(property) method that essentially is a more terse version of the above.

import {
  validatePresence,
  validateLength,
} from 'ember-changeset-validations/validators';
import validateSometimes from 'ember-changeset-conditional-validations/validators/sometimes';

export default {
  creditCardNumber: validateSometimes(
    [validatePresence(true), validateLength({ is: 16 })],
    function (changes, content) {
      return this.get('paymentMethod.isCreditCard');
    }
  ),
};

I have made this available as an addon called ember-changeset-conditional-validations if you're interested in using it. If you've handled conditional validation differently, let me know how you did it in the comments!