Opinions asked: what is best way to validate/highlight Ember form elements?


#1

I am used to work with JQuery.validation to do validations of form elements. Now that I start using Ember views, I wonder what the best way is to validate fields, highlight fields, show error messages (individual per field or grouped), etc.

I see 4 possibilities:

1. Use Jquery.validation as before: thus in the document.ready, initialize the form validation settings and behavior.

$

$("#registerForm").validate({
    rules : {
        user_name : "required",
        user_email : {
            required : true,
            email : true
        },
    },
    errorClass : "help-inline",
    errorElement : "span",
    highlight : function(element, errorClass, validClass) {
        $(element).parents('.control-group').removeClass('success');
    },
    unhighlight : function(element, errorClass, validClass) {
        $(element).parents('.control-group').removeClass('error');
    }
});

});

2. Create specific Ember views for each individual form element (input, textarea, etc.) and implement highlighting in the view code; but what with the error message in that case ?

3. Create specific Ember views for a group of elements (e.g. a control-group representing a div, an i, an input and a span element and implement highlighting in the view code and also implement show and hide of error messages in that view.

4. Put field validation logic in the controller.

In addition to the above, if the server comes back with a 422 error (server validation), I want to use the same highlighting and error text message possibilities. I think option 3 or 4 is the best way to go, but as I am new to Ember, I would welcome any suggestions/ideas or even better: some code samples.


#2

I’m surprised no one else has replied to this, validation in Ember currently feel pretty bulky with the easiest way right now seeming to be to make custom form fields for every data type and porting code from validator to run within the action callback.

One option I’m exploring extends ember-data's attribute types to accept either an object or a string. Then if the type or validation function failed it would throw a VALIDATION_ERROR exception that could be caught and used to display a message. I’m really not sure this is the best place for that though, because we aren’t trying to validate all data, we’re just trying to validate data changes.

Another option, and probably the better method given that not everyone uses ember-data I’m thinking would be to add jQuery.validate as a Handlebars helper but with an added namespace controller . Something like {{validatedInput rule=view.rules.fooField namespace='myFormID'}}. You would then specify the rules within the view’s controller.

I haven’t tested this yet, but canceling the event if validation fails is probably enough to prevent changes to the model in DS.store, if not, I’d probably use {{unbound}} in the handlebars template and only update the field manually if validation passed. I wouldn’t setup validate to ignore invalid strings (e.g. not update the value, and therefore not update the store) because sometimes validation should only be soft (for example with zipcodes, 5 digits but not a known zipcode could still be valid and is therefore allowed with caution).


#3

This person thinks ember-data shouldn’t validate at all: Proposal: Remove model validity/server side error handling from ember-data

Which also points out there’s the mixin route: https://github.com/dockyard/ember-validations.

This older discussion also has some other approaches:


#4

I really like how ember-data puts 422 error responses onto the model automatically. I use a pretty simple method to display validation errors in my forms.

I simply use bind-attr (e.g., <p {{bind-attr class="field errors.fieldName:errors}}>) to set an error class, and I iterate over the errors to display the messages.

This all gets triggered by the save action, which lives in my router. On success, the router navigates to the appropriate location, but on error it just stays on the form and errors are displayed through data binding.

Currently I rely on the server-side validation, but once I do go to put in client-side validation, I would hope that I could use the same method.


#5

It seems to me that this is an area in flux.

As you said, ember-data sets the errors hash received from the backend directly as the errors property of the record. That has a few disadvantages (the biggest among them being that it clobbers errors that might have been set on the client-side) and ember-data master removed that direct setting, passing the responsibility of dealing with the errors to the app developer. They also introduced a DS.Errors class that gets set on each model and that has a rich interface to add/remove/etc. errors.

ember-validations does not play nicely with ember-data because it redefines isValid and errors, causing all kinds of weird scenarios. This seems to be a known issue. Also, as far as I can tell, it does not deal with “server-side validations”, like unique username. (please correct me if I am wrong, @bcardarella)

Since in the current project, we use ember-data 1.0.0.beta.4, I cobbled together my own solution, very slightly modifying ember-data behavior and defining an errors property on models in the application code. This way the code that deals with validation can stay quite simple although it is not general enough yet.


#6

I did an investigation of ember-validations a while back and it was very lacking at the time. Not sure if it has gotten better, but here is the post I made:

There is a jsFiddle in there: http://emberjs.jsbin.com/iDeQABo/2/ that shows the ideas. For some reason it won’t load the edit page, but you can grab the code from the output directly by inspecting in chrome.

The basic ideas was instead of making a very prescriptive approach like ember-easy form, it’s better to have opt-in design that integrates very well with traditional use of handlebars. You just add properties to the Ember out of the box fields and the parent view will look at them, then add computed properties for each, which you can then reference in the template as if you had written them explicitly. There might be some work to deal with name collissions, but you get the general idea.

Main part of code that does the ‘magic’:

	didInsertElement: function() {
	Ember.run.scheduleOnce('afterRender', this, 'processChildElements');
},

processChildElements: function() {
	// ... do something with collectionView's child view
	// elements after they've finished rendering, which
	// can't be done within the CollectionView's
	// `didInsertElement` hook because that gets run
	// before the child elements have been added to the DOM.

	var childViewsThatRequireValidation = this.get('childViews').filter(function (view) {
		if(view.hasOwnProperty("validateOn")) {
			return view;
		}
	});

	childViewsThatRequireValidation.forEach(function (view, index, views) {
		view.set("canShowValidationError", false);
		view.set("hasFocusedOut", false);

		// TODO: use action from the validateOn paremter
		view.set("focusOut", function (event) {
			this.set("canShowValidationError", true);
		});

		var bindingFrom = view.get('valueBinding._from');
		var modelProperty = bindingFrom.match(/\.(\w+)$/i)[1];
		var errorsForPropertyString = 'parentView.controller.errors.%@'.fmt(modelProperty);
		var computedPropertyKey = '%@.@each'.fmt(errorsForPropertyString);
		var errorsForPropertyLength = '%@.length'.fmt(errorsForPropertyString);
		var errorsForProperty = view.get(errorsForPropertyString);
		var computedFunction = Ember.computed(function() {
			return (this.get("canShowValidationError") && (this.get(errorsForPropertyLength) >0));
		}).property('value', 'canShowValidationError');

		Ember.defineProperty(view, "showError", computedFunction);

		// For some reason this is required in order for the computed property to update
		// even though after this call, the computed property is still not consumed
		// I'm suspicious that the UI isn't updated because we are in the 'afterRender' section of the run loop queue
		// and this update is not propegated back to the UI.
		view.get("showError");
	});
}