How should input validation be done with Ember?

I have tried and done this so far in a few ways, but have no clue if there is a more Ember-way to do this. On of the ways I did is create a controller and use for explicit input validation calls. Error would be set (properties of this controller) and previously bound to views. This way the errors would be displayed instantly. The other way was similar to this, only I validated model values.

But none of these are actually nice enough. A great way of doing this would be to have it done in a bit more automated way. Maybe just before commit, or by observing values and have it done when they are invalid.

Is there an expected/proposed way of doing this with Ember?

3 Likes

I’d be curious what the core team has to say about this. I’ve turned my “view” into a form of sorts when I need to do basic input validation.

In this way the template is still bound to the model through the controller, but I found the javascript view itself didn’t do much (aside from holding the templateName that I never needed to override) so I found adding input validation was a good fit.

Take this simple handlebars form

  {{view Ember.TextField placeholder='score' valueBinding='view.score'}}
  {{view Ember.TextField placeholder='feedback' valueBinding='view.feedback'}}
  <a {{action addRating model target='view' href=true}}>add rating</a>

To validate the inputs / reset them after a new item is added I do the work in the view itself (leaving the controller to handle other computed properties / model helpers)

CodeCamp.SessionView = Ember.View.extend({
  templateName: 'session',
  addRating: function(event) {
    if (this.formIsValid()) {
      var rating = this.buildRatingFromInputs(event);
      this.get('controller').addRating(rating);
      this.resetForm();
    }
  },
  buildRatingFromInputs: function(session) {
    var score = this.get('score');
    var feedback = this.get('feedback');
    return CodeCamp.Rating.createRecord(
    { score: score,
      feedback: feedback,
      session: session
    });
  },
  formIsValid: function() {
    var score = this.get('score');
    var feedback = this.get('feedback');
    if (score === undefined || feedback === undefined || score.trim() === "" || feedback.trim() === "") {
      return false;
    }
    return true;
  },
  resetForm: function() {
    this.set('score', '');
    this.set('feedback', '');
  }
});

I’ve found that my server side platform of choice has a similar feel where html templates are the equivalent to handlebars but a special “form” is used to handle html inputs and validation separate from the controller and model. (django)

This type of separation also makes it easy to unit test the inputs and events that bubble up from the view.

2 Likes

This is an interesting approach. I like it better than what I did. Also, I am curious on core team members take on the subject.

1 Like

Sometime ago I created a class like so:

    TC.Form = Ember.View.extend({
    classNames: "ember-form",

    formFields: Ember.computed(function() {
      var childViews = this.get('_childViews');

      var ret = Ember.A();

      Ember.EnumerableUtils.forEach(childViews, function(view) {

      if (view.isFormField) {
        ret.push(view);
      }
      });

      return ret;
    }).property(),

    clearErrors: function() {
      var form_items = this.get('formFields');
      this.$(".error .help-inline").remove();
      for(var i = 0; i < form_items.length; i++)
      {
        form_items[i].$().parents(".control-group").removeClass("error");
      }                
    },

    validate: function() {
      var errors = 0;
      var formFields = this.get('formFields');
      this.clearErrors();

      for (var i = 0; i < this.validators.length; i++)
      {
        var validator = this.validators.objectAt(i);
        var field = this.get(validator.field);
        var func = Ember.typeOf(validator.validator) == 'string'? TC.Form.defaultValidators[validator.validator] : validator.validator;
        var message = validator.message;
        var field_valid = func(field, this, validator);
        if (!field_valid) {
          this.error(field, message);
          errors++;
        }
        else {
           this.valid(field);
        }
      }
      return errors === 0;
    },

    valid: function (field) {
       if (field) {
        field.$().parents(".control-group").removeClass("error");
        field.$().next(".help-inline").remove();
      }
    },
    error: function(field, message) {
      if (field) {
        field.$().parents(".control-group").addClass("error");

        if (field.$().next(".help-inline").length === 0)
          field.$().parents(".controls").append("<span class=\"help-inline\">" + message + "</span>");
      }
    }
  });

Here a example of how to use it:

 {{#view TC.Form class="form-horizontal" id="city_form" validatorsBinding="controller.validators" viewName="form"}}
    <fieldset>
      <div class="control-group">
        <label class="control-label">* Nome:</label>
        <div class="controls">{{view TC.TextField valueBinding="controller.selected.name" viewName="name"}}</div>
      </div>
    </fieldset>
    <div class="form-actions">
      <button {{action "save"}} class="btn btn-primary">Salve</button>
      <button {{action "back"}} class="btn">Cancel</button>
    </div>
{{/view}}

And in your controller:

         validators: [
            { field: 'name', validator: 'required', message: "Name is mandatory" }
        ]

I haven’t really thought about it and created it to solve my problems at the time. But here are some pros/cons that are obvious for me:

PROS:

  • You can easily subclass error(), valid() and clearErrors() to use your html structure, in this example it uses the bootstrap html. You could easily add all errors on the top of the form by doing something alike in your error() method:

       if (this.$(".errors").length == 0) {
         this.$().prepend("<ul class=\"errors\"></ul>");
       }
       this.$(".errors").prepend("<li>" + message + "</li>");
    
  • Your validators stay in your controller and how to handle errors in the view.

CONS:

  • You need to subclass field views to insert a isFormField field so TC.Form knows which views are form items and which ones aren’t.

Here another approach by dockyard: https://github.com/dockyard/ember-validations

1 Like

If you are a component :slight_smile: user, you could try out ember-validate.

2 Likes