On Meteor, how do I validate with Collection2 on the client side?

advertisements

I always use methods to insert, update and remove. This is the way my code look just now:

Client side

Template.createClient.events({
  'submit form': function(event, tmpl) {
    e.preventDefault();
    var client = {
      name: event.target.name.value,
      // .... more fields
    }

    var validatedData = Clients.validate(client);
    if (validatedData.errors) {
      // Display validation errors
      return;
    }

    Meteor.call('createClient', validatedData.client, function(error) {
      if (error)
        // Display error
    });
  }
});

Client and server side:

Clients = new Mongo.Collection("clients");

Clients.validate = function(client) {
  // ---- Clean data ----
  client.name = _.str.trim(client.name);
  // .... more fields clean

  // ---- Validate data ---
  var errors = [];
  if (!client.name)
    errors.push("The name is required.");
  // .... more fields validation

  // Return and object with errors and cleaned data
  return { errors: _.isEmpty(errors) ? undefined : errors, client: client };
}

Meteor.methods({
  'createClient': function (client) {
    // --- Validate user permisions ---

    // If server, validate data again
    if (Meteor.isServer) {
      var validatedData = Clients.validate(client);
      if (validatedData.errors)
        // There is no need to send a detailed error, because data was validated on client before
        throw new Meteor.Error(500, "Invalid client.");
      client = validatedData.client;
    }

    check(client, {
      name: String,
      // .... more fields
    });

    return Clients.insert(client);
  }
});

Meteor.call is executed on client and server side, but Meteor doesn't have a way stop the running on the server side if the validation on the client side fails (or at least, I don't know how). With this pattern, I avoid sending data to the server with Meteor.call if validation fail.

I want to start using Collection2, but I can't figure how to get the same pattern. All the examples I found involve the usage of direct Insert and Update on client side and Allow/Deny to manage security, but I want to stick with Meteor.call.

I found on documentation that I can validate before insert or update, but I don't know how to get this to work:

Books.simpleSchema().namedContext().validate({title: "Ulysses", author: "James Joyce"}, {modifier: false});

I know the autoform package, but I want to avoid that package for now.

How can I validate with Collection2 on the client side before sending data to the server side with Meteor.call? Is my pattern wrong or incompatible with Collection2 and I need to do it in another way?


In under 30 lines you can write your very own, full-featured validation package for Collection2. Let's walk through an example:

"use strict"; //keep it clean
var simplyValid = window.simplyValid = {}; //OK, not that clean (global object)
simplyValid.RD = new ReactiveDict(); //store error messages here

/**
 *
 * @param data is an object with the collection name, index (if storing an array), and field name, as stored in the schema (e.g. 'foo.$.bar')
 * @param value is the user-inputted value
 * @returns {boolean} true if it's valid
 */
simplyValid.validateField = function (data, value) {
  var schema = R.C[data.collection]._c2._simpleSchema; //access the schema from the local collection, 'R.C' is where I store all my collections
  var field = data.field;
  var fieldVal = field.replace('$', data.idx); //make a seperate key for each array val
  var objToValidate = {};
  var dbValue = schema._schema[field].dbValue; //custom conversion (standard to metric, dollars to cents, etc.) IGNORE

  if (dbValue && value) value = dbValue.call({value: value}); //IGNORE
  objToValidate[field] = value; //create a doc to clean
  schema.clean(objToValidate, {removeEmptyStrings: false}); //clean the data (trim, etc.)
  var isValid = schema.namedContext().validateOne(objToValidate, field, {extendedCustomContext: true}); //FINALLY, we validate
  if (isValid) {
    simplyValid.RD.set(fieldVal, undefined); //The RD stores error messages, if it's valid, it won't have one
    return true;
  }
  var errorType = schema.namedContext()._getInvalidKeyObject(field).type; //get the error type
  var errorMessage = schema.messageForError(errorType, field); //get the message for the given error type
  simplyValid.RD.set(fieldVal, errorMessage); //set the error message. it's important to validate on error message because changing an input could get rid of an error message & produce another one
  return false;
};
simplyValid.isFieldValid = function (field) {
  return simplyValid.RD.equals(field, undefined); //a very cheap function to get the valid state
};

Feel free to hack out the pieces you need and shoot me any questions you might have.