TIL: Async custom ember-CP-validations with ember-concurrency

Validations in Ember.js are easy when using the ember-cp-validations-addon. With that great addon it even isn't too hard to create custom-validators that check validations via a remote request. If we needed to check email-uniqueness in a form we could for example create a custom validator unique-email:

// validators/unique-email.js

import Ember from 'ember';  
import BaseValidator from 'ember-cp-validations/validators/base';

const { inject: { service }, get } = Ember;

export default BaseValidator.extend({  
  // ember-ajax
  ajax: service(),

  validate(value) {
    let ajax = get(this, 'ajax');

    return ajax.request('/api/validations', {
      data: {
        email: value
      }
    })
    .then((result) => {
      // handle the validation result im some way, e.g.
      if (result.error) {
        return result.error;
      }
    })
    .catch(() => {
      // do whatever is useful here
    }
  }
});

But as everybody that has implemented something like that you will realize that we will run into issues here as we most likely want to validate user input while users are typing and this will fire multiple requests which may or may not send their data in the expected order etc.

Of course there's the incredibly useful ember-concurrency-addon available to deal with concurrent problems like that and fortunately it's really easy to integrate ember-concurrency and ember-cp-validations as ember-concurrency's TaskInstances are thenable:

// validators/unique-email.js

import Ember from 'ember';  
import BaseValidator from 'ember-cp-validations/validators/base';  
import { task, timeout } from 'ember-concurrency';

const { inject: { service }, get } = Ember;

export default BaseValidator.extend({  
  // ember-ajax
  ajax: service(),

  validationTask: task(function* (value) {
    // debounce validation requests
    yield timeout(300);  

    let ajax = get(this, 'ajax');

    yield ajax.request('/api/validations', {
      data: {
        email: value
      }
    });
  }).restartable(),

  validate(value) {
    if (value) {
      return get(this, 'validationTask').perform(value);
        .then((result) => {
          // handle the validation result im some way, e.g.
          if (result.error) {
            return result.error;
          }
        })
        .catch(() => {
          // TaskCancellations are caught here as well
        }
  }
});

This can then be used like any custom validator:

// some component rendering a form
import Ember from 'ember';  
import { buildValidations, validator } from 'ember-cp-validations';

const { Component } = Ember;

const Validations = buildValidations({  
  email: validator('unique-email')
});

export default Component.extend({  
  Validations,

  // create a form-object that validates via `Validations`
  // ...
})

When you want to display a loading indicator while validations are in progress this is very easy to do as well as you have access to validations.attrs.email.isValidating. 🚀 🍻

Though a quick TIL this time I still hope this was helpful, thanks for reading and as always just drop me a line on twitter (@LevelbossMike) or ping me on the Ember.js-slack if you have questions. I am also available for consulting work and would be happy to help you and your company with your Ember.js-projects.

comments powered by Disqus