One of my absolute favorite things about ExtJS’ data model is how simple it is to apply a Model instance to a form. If you’ve never done this before, consider the following data model:

Ext.define('Validations.model.Person', {
    extend: 'Ext.data.Model',
    fields: [
        {name:"name", type:'string'},
        {name:"age", type:"int"},
        {name:"hobby", type:"string"}
    ]
})

Also, assume we have a form defined like so:

Ext.define('Validations.view.people.Form', {
    extend: 'Ext.form.Panel',
    alias: 'widget.peopleform',
    bodyPadding: 10,
    border: false,
    initComponent: function() {
        var me = this;
        Ext.applyIf(me, {
            defaults: {
                width: 250,
                labelWidth: 70
            },
            items: [
                {
                    xtype: 'textfield',
                    name: 'name',
                    fieldLabel: 'Name'
                },
                {
                    xtype: 'numberfield',
                    name: 'age',
                    fieldLabel: 'Age',
                    value: 20
                },
                {
                    xtype: 'textfield',
                    name: 'hobby',
                    fieldLabel: 'Hobby'
                }
            ]
        });
        me.callParent(arguments);
    }
})

NOTE: Notice that the “name” properties of each form field maps 1:1 with the “name” properties in my Model.

Now, let’s say that we want to edit a Model instance. We could, of course, manually set the value of each form field to that of the corresponding field in the Model. However, this is clunky and unnecessary, given that the form itself has a method for doing this automatically. Consider the following:

// get first model instance in store
 myrecord = somestore.getAt(0)
 // load form with data from selected model instance
 myform.loadRecord(myrecord)

Pretty simple, right? When calling the loadRecord() method on the form, any field in the form whose name value matches the name definition in the specified Model will be loaded with the data of that particular field.

Try it out. Open the demo, and double-click on any record in the grid. On the itemdblclick event of the grid, the form’s loadRecord() method is invoked, and the Model data corresponding to the clicked row in the grid is applied to the form’s fields

Validation, Validation, Validation

So that’s pretty easy. But of course, when dealing with data–whether on the Model or in a form–you’re going to want to validate it to make sure that you have the data you’re expecting, and that the data is in the right format.

In ExtJS, there are a few different ways you can validate Model data.

First, you can apply validations directly to the model itself. For example, let’s add in a few to the Model already setup:

Ext.define('Validations.model.Person', {
    extend: 'Ext.data.Model',
    fields: [
        {name:"name", type:'string'},
        {name:"age", type:"int"},
        {name:"hobby", type:"string"}
    ],
    ,
    validations: [
        {type: 'presence', field: 'name', message: 'You have to enter a name, silly'},
        {type: 'presence', field: 'age', message: 'You must specify an age'},
        {type: 'presence', field: 'hobby', message: 'You must enter a hobby'},
        {type: 'length', field: 'hobby', min:5, message: 'You must specify a hobby with more than 4 characters'}
    ]
})

In this example, I’ve applied a “presence” validation to each field, which requires that each field has a value, as well as a “length” validation to the “hobby” field. When calling the Model’s validate() method, the instance’s current data will be run through the validations.

The second, albeit indirect, way to validate the Model is to apply validations to the form fields themselves, such as defining properties like allowBlank (e.g., “presence”), specifying a custom vType for the field, using a regex, etc.

While both of these approaches are great, they are not particularly related. For example, if we have NO validations whatsoever on the form, we could submit perfectly valid Form data, but this data may yet trigger validations when the Model is validated. By default, the form doesn’t care about the Model-level validations, and will only trigger Form-level validations that have been defined.

Applying Model Validation to Form Fields

While this disconnect is certainly understandable, I thought it would be cool if both validations could be supported when the form is being validated. Here’s a short list of my requirements:

  • Model validations should be enforced at a field by field level (e.g., if there are two validations for “name”, these should both be enforced on the “name” field in the form)
  • Both Model and Form field validations should be respected
  • Both Model and Form field validations should be communicated to the user

So how to do this? While there are probably a dozen ways to accomplish this goal, the approach I took was to simply override Ext.form.Basic and Ext.form.field.Base.

The first step is to make the form aware of Model validations. Since we really only care about this when the Model data is loaded to the form via loadRecord(), we can start by overriding this method like so:

Ext.override(Ext.form.Basic, {
    loadRecord: function(record) {
        this._record = record;
        this.setModelValidations(record.validations);
        return this.setValues(record.data);
    },
    setModelValidations: function(validations) {
        var fields = this.getFields(), i;
        for(i=0;i<validations.length;i++) {
            // updated 11/23/12...get field based on lookup
            // don't assume id is set on field
            var fieldMatch = this.findField(validations[i].field);
            if(fieldMatch) {
                fieldMatch.setModelFieldValidation(validations[i])
            }
        }
    }
})

When loadRecord() is invoked, we pass the record to a new custom method, setModelValidations(). This custom method will iterate over the validations object of the specified Model, and will invoke the setModelFieldValidation() method of any form field it finds whose mapping matches that of the Model validation.

The next override is on the base definition for form fields, and looks like so:

Ext.override(Ext.form.field.Base, {
    setModelFieldValidation: function(validation) {
        this.modelValidations = Ext.isArray(this.modelValidations) ? this.modelValidations : [];
        this.modelValidations.push(validation);
    },
    getModelErrors: function(value) {
        var errors      = Ext.create('Ext.data.Errors'),
            validations = this.modelValidations,
            validators  = Ext.data.validations,
            length, validation, field, valid, type, i;

        if (validations) {
            length = validations.length;

            for (i = 0; i < length; i++) {
                validation = validations[i];
                field = validation.field || validation.name;
                type  = validation.type;
                valid = validators[type](validation, value);

                if (!valid) {
                    errors.add({
                        field  : field,
                        message: validation.message || validators[type + 'Message']
                    });
                }
            }
        }
        return errors;
    },
    validateValue: function(value) {
        var me = this,
            errors = me.getErrors(value),
            modelErrors = me.getModelErrors(value),
            isValid = Ext.isEmpty(errors) && modelErrors.isValid();
        if (!me.preventMark) {
            if (isValid) {
                me.clearInvalid();
            }
            else {
                if(!modelErrors.isValid()) { modelErrors.each(function() { errors.push(this.message); }) }
                me.markInvalid(errors);
            }
        }
        return isValid;
    }
})

There’s a lot going on here, so let’s go method by method.

setModelFieldValidation(): This is method that is called from the Form’s setModelValidations() method, and it simply adds a new property to the instance of the form field (modelValidations). This modelValidations is an array which will store any Model-level validations which have been configured for the Model field which matches this form field’s “name”.

getModelErrors(): This method is almost identical to the Model’s getErrors() method. The primary difference is that instead of iterating over every validation defined for the Model, this method only iterates over the Model validations which have been assigned to this form field (via setModelFieldValidations()).

validateValue(): This is an override of the field’s existing validateValue() method, which is invoked everything a validation occurs. The only additions are to add the Model validation to the check for whether the form “isValid”, as well as a short section to append the model validation error messages to the main “errors” array (which is ultimately used for communicating the form’s validation errors to the user).

Update (11-23-12)

As Meteo rightly noted in the comments, this only works if the “id” property is defined in the config for the form fields. I’ve updated the Basic form override and Git source to accomodate this scenario.

View this on JSFiddle

Demo and Source

If you’d like to see this in action, check out the demo. If you’d like to see the full source for this mini-app, grab it from GitHub.

Wrapping Up

In all honesty, I’m not sure how practical this approach actually is, or whether you’d actually want to forge this relationship between the form and model validations. But regardless of the practicality, I think it does, at the very least, demonstrate precisely how stupid simple it is to extend ExtJS to do pretty much whatever you want it to do. And notwithstanding the usefulness of this particular example, this extensibility is precisely why I love this framework.