Validation (The M Project 1.x) - mwaylabs/The-M-Project GitHub Wiki

Introduction

Validation is one of the features in the framework that helps app developers to have consistent data inside the application. Simply said, it validates data against a certain definition that is defined by the developer. Validation can take place in the view as well as in the model. For example in the view, the app developer might want to have a form not be submitted until all necessary input in the textfields is given and that the input of the user in an email field is syntactically an email adress. In MVC the model is the gatekeeper to persistence. the model represents and persists the application's business data. With this task in mind it makes sense that models validate themselves before persisting into storage. The other way around this means, all data that is persisted by the model is valid and all model objects fetched from storage are valid objects.

Who's doing the job?

The Validation is done by the so called Validators which are instances of the prototype object M.Validator. There are several predefined validators shipped with the framework that can be used out of the box:

  • M.DateValidator: Checks if it is possible to create a date from the value by using it with M.Date.create(), which means if YES then it is a date.
  • M.EmailValidator: Checks if the given string satisfies the email adress pattern.
  • M.NotMinusValidator: Checks if a value is a negative number. Works with strings as well as with numbers.
  • NumberValidator: Checks if a value represents a number. Works with strings as well as with numbers. (Object type validation can be done with typeof(), e.g. (typeof(1) === 'number').
  • M.PhoneValidator: Checks if a value represents a phone number.
  • M.PresenceValidator: Checks if a value is given.
  • M.UrlValidator: Checks if a value is a valid URL.

How to use a validator with a model?

The model prototype object defines a boolean property usesValidation which defaults to YES. That means the model is validated before it is saved. But what validators are used? The developer needs to name them on model definition. Let's assume we have a model Contact with the following properties:

  • firstName
  • lastName
  • email
  • age

Our model definition now looks like this (our app is named Contacts):

Contacts.Contact = M.Model.create({
  __name__: 'Contact' // generated by model generator

  firstName: M.Model.attr('String', {
    isRequired: YES
  },

  lastName: M.Model.attr('String', {
    isRequired: YES
  },

  email: M.Model.attr('String', {
    validators: [M.EmailValidator]
  },

  age: M.Model.attr('Integer', {
    validators: [M.NumberValidator, M.NotMinusValidator]
  } 
}, M.DataProviderWebSql.configure({
  dbName: 'contacts_db'
}));

As you can see, validators are named for each property by setting an array of validators to the validators property in the parameter object passed to the attr method of the model. This method returns an object of M.ModelAttribute that contains all this information we pass here. Here, email and age are validated with the named validators. Naturally, if no validators are set, no validation is performed for this property. It seems that the model properties firstName and lastName are not validated because they don't have any validators passed. At first sight this is true. At the second sight we see the property isRequired set to YES. Isn't this also a validation target? Yes, it is: all model properties that have isRequired set to YES automatically get the M.PresenceValidator added to their validators array. Using isRequired is just a more readable way for this very important property constraint. The property is set to NO by default. Not setting it to YES and naming the M.PresenceValidator in the validators array has the same impact as setting the property to YES.

How to use a validator with a view?

Instead of validating on the model's side, it is also possible to use validation with a view. Since model validation is performed right before saving it, a validation in the view's side can prevent you from creating an invalid record at all. Other frameworks and technologies typically would call this input validation.

Using validation with a view is basically the same as with a model: You need to set the validators property and specify the validators you want to use – either some of the predefined validators or your own. To simplify the validation process itself, you have to use a wrapper view element called M.FormView. This wrapper then can be used to easily trigger the validation on all of the M.FormView's child views. So for example a single M.TextFieldView with a M.PresenceValidator would be defined like this:

form: M.FormView.design({
    firstName: M.TextFieldView.design({
        name: 'firstName',
        label: 'Firstname',
        validators: [M.PresenceValidator.customize({
            msg: 'Firstname is required'
        })]
    })
});

Once you have specified some validators for your views that are wrapped inside an M.FormView container, you can simply trigger the validation out of a controller by calling the validate() method of M.FormView. So let's say you submit your input elements on a button click to a controller method. Then this method could look something like:

readInput: function() {
    if(!M.ViewManager.getView('page', 'form').validate()) {
        if(M.Validator.validationErrors) {
            M.ViewManager.getView('page', 'form').showErrors();
            return;
        }
    }

    // if the validation returns true, do what ever you want to do with your input data...
}

As you can see, the validate() method of M.FormView returns either true or false. If it returns true, everything is fine! If false is returned, at least one validation went wrong. Then you can use the validationErrors property of M.Validator to get further knowledge about which validation(s) went wrong. This property is a simple array containing information about every single validation that returned false. A even more simple solution is to use the showErrors() of your M.FormView. This will automatically show an M.AlertDialogView, that displays the specified error messages for all invalid validations.

If you need a more custom behaviour, you will have to iterate through mentioned validationErrors property of M.Validator. Such an error, e.g. for the M.TextFieldView of our example, will look like:

msg: "Firstname is required"
validator: "PRESENCE"
viewId: "m_01"

So you will know which view caused the validation error (viewId), which validator caused the error (validator) and what message was defined (msg). This allows you to implement your own custom and even M.FormView specific behaviour for handling validation errors, since every M.FormView can be handled differently.

How do I write my own validator?

Naturally, the framework cannot deliver validators for every imaginable task. Luckily it is easily possible to write your own validators. Basically all you have to do is extend M.Validator.

  M.MyOwnValidator = M.Validator.extend({
    validate: function(obj) {
      // your validation code here...
    } 
  });  

The one thing you need to implement inside your validator is a function named validate which takes an object as parameter (contains the value that's going to be validated) and returns a boolean (YES / NO). Save your code to a file and put it into a validators directory inside your application (next to controllers, views, ...). Alternatively it is possible to define your own validators inline with the model definition:

   zip: M.Model.attr('String', {
    validators: [M.NumberValidator, {
       validate: function(obj) { if obj.value.length > 5) { return NO; } return YES; }
    }]
  } 

This validator checks if the string provided for the zip code property has more than 5 characters (we will not discuss if this makes sense here) and returns NO (validation failed) if it is more than five and YES (validation passed) if not.

How can I customize a given validator?

It is possible to customize an existing validator by using the customize function that every validator has. Just pass it the parameters you want to change inside the validator: it is also possible to pass another validate function.

   zip: M.Model.attr('String', {
    validators: [M.NumberValidator.customize({
      msg: 'Not a valid zip code. Please enter another one.' // defines your own message that is written to the error buffer
    })]
  } 

You have to weigh whether to write your own validator or customize an existing one. Surely, customizing an existing one only changes the validator for the one property, not for all properties where the validator is used.

Validation workflow with model.save()

width=900px