backbone.validation icon indicating copy to clipboard operation
backbone.validation copied to clipboard

Validation with 2 views using the same model

Open sdeleuze opened this issue 11 years ago • 7 comments

I don't know if it is (1) currently possible, (2) a possible evolution or (3) imposible by design, but is it possible to validate 2 views using the same model and displayed at the same time ?

Backbone is designed to allow this easily thanks to binding on model or collection, but is it possible with current Backbone.Validation.bind and Backbone.Validation.unbind behaviour ?

sdeleuze avatar Jun 19 '13 17:06 sdeleuze

I don't see why it shouldn't work, but I haven't tried it myself. If you do, please let me know the result:-)

-Thomas P

On 19. juni 2013, at 19:14, "Sébastien Deleuze" [email protected] wrote:

I don't know if it is (1) currently possible, (2) a possible evolution or (3) imposible by design, but is it possible to validate 2 views using the same model and displayed at the same time ?

Backbone is designed to allow this easily thanks to binding on model or collection, but is it possible with current Backbone.Validation.bind and Backbone.Validation.unbind behaviour ?

— Reply to this email directly or view it on GitHubhttps://github.com/thedersen/backbone.validation/issues/140 .

thedersen avatar Jun 19 '13 17:06 thedersen

I was wondering about conflict on view1.model and view2.model if I do : Backbone.Validation.bind(view1); Backbone.Validation.bind(view2); // Does it break bind on view1 since ti use the same model ?

Or was wondering if validation should be disabled on both views as soon as I unbind of them : Backbone.Validation.bind(view1); Backbone.Validation.bind(view2); Backbone.Validation.unbind(view2); // Does validation still work on view1 ?

I will try ...

sdeleuze avatar Jun 19 '13 17:06 sdeleuze

Issue confirmed, you can test it with the example from https://github.com/sdeleuze/backbone.validation/tree/issue140

It works with : this._render(new BasicView({ model: new BasicModel()})); this._render2(new BasicView({ model: new BasicModel() }));

It fails with : var basicModel = new BasicModel(); this._render(new BasicView({ model: basicModel })); this._render2(new BasicView({ model: basicModel }));

Any thoughts ?

sdeleuze avatar Jun 26 '13 12:06 sdeleuze

Running into this issue as well. I have several views that operate on the same model. The problem is the way the validation function is set up with a model. When you call Backbone.Validation.bind, the model's validate function is replaced each time. This means each time you call bind, the association with the previous view is completely lost.

Fixing this is not super simple. It would require changing the bindModel function to somehow queue the views, and changing validate method to support multiple views. I don't think it would require a complete overhaul, however.

nynexman4464 avatar Aug 15 '13 19:08 nynexman4464

I'm also running into this issue. I have one model that's used by several different views. For instance, I have a view that displays an input for username. I have separate views for email address, passwords, etc. In my own use case, each one of these views need to be separated with their own display logic, so having only one view isn't an option. These all use the same model and all these views have validation rules set up on the model.

All the model attributes still validate correctly when isValid(true) is called when the "Sign Up" button is clicked, but just as @nynexman4464 said, the last view to bind is the only one passed into the valid and invalid callbacks. I forked @thedersen 's original example jsfiddle from the Backbone.Validation documentation to demonstrate this issue. Example fiddle here: http://jsfiddle.net/wesleymusgrove/WVgpk/15/

Based on this fiddle, the Terms of Use view is the only one bound. In the invalid and valid callbacks, any attempt to set the .has-error class on the view's closest .form-group fails because a reference cannot be created to elements that don't exist within the Terms of Use view, like $el = view.$("[name=username]")

View the console to verify the preValidate messages being returned and the final result of validation.

wesleymusgrove avatar Jan 13 '14 21:01 wesleymusgrove

I have a project where I need to do something similar. I haven't verified this but I am going to try just using Validation mix-in and then write some glue code to get the views to render the errors.

SomethingSexy avatar Mar 24 '14 20:03 SomethingSexy

@SomethingSexy, I have found a solution for this issue and have implemented it in a few of my projects and can verify it works. Check out this fiddle: http://jsfiddle.net/wesleymusgrove/CyJ2y/

According to the documentation, I'm mixing in the Validation with the Model's prototype:

_.extend(Backbone.Model.prototype, Backbone.Validation.mixin);

Backbone Marionette's EventAggregator was the "glue code" I chose to use in order to get all my different views to listen for model validation events.

var vent = new Backbone.Wreqr.EventAggregator();

Firstly, the signUpClicked event is triggered if there's anything that the views need to do when that button gets clicked. In this case that event listener just writes something out to the console.

var ButtonView = Backbone.View.extend({
    events: {
        "click #signUpButton": "signUpButtonClicked"
    },

    signUpButtonClicked: function(e) {
      vent.trigger("signUpClicked");
      this.model.validateSignupModel(this);
    }
});

After that, I'm calling validateSignupModel once and only once per button click, which validates the entire model.

validateSignupModel: function(view) {
      if(view.model.isValid(true)){
            console.log("VALIDATION = Great Success!");
      }
      else {
          console.log("VALIDATION = Invalid);
      }
    }

In response to the isValid call on the entire model, I'm binding to the validated event and am triggering events that make the status of the model validation known to whatever view is listening:

initialize: function() {
      this.bind('validated', function(isValid, model, errors) {
        if (isValid) {
          vent.trigger("model:valid", model, errors);
        }
        else {
          vent.trigger("model:invalid", model, errors);
        }
      });
    }

Looking at the fiddle, you can see that each view extends from BaseView and is listening for those "model:valid" and "model:invalid" events:

var BaseView = Backbone.View.extend({
    initialize: function(){
        var self = this;
        //DO NOT BIND THE VIEW TO THE MODEL
        //Backbone.Validation.unbind(self);
        //Backbone.Validation.bind(self);

        vent.on("signUpClicked", function() {
            //console.log("signUpClicked trigger validation");
        });

        vent.on("model:valid", function(model) {
            //console.log("model is VALID", self, model);
            self.model.clearErrors(self);
        });

        vent.on("model:invalid", function(model, errors) {
            //console.log("model is INVALID", self, model, errors);
            self.model.clearErrors(self);
            self.model.markErrors(self, errors);
        });
    } 
});

The clearErrors and markErrors functions on the model control when the valid and invalid Backbone.Validation.callbacks get called so that the view and attr passed into those callbacks will match up with the name attribute of the actual input element. (For example, if you pass an instance of the UserNameView and the "username" model attribute into clearErrors and markErrors, they will try to find the input element with a name attribute that matches the model attribute you passed in: <input type="text" class="form-control" id="username" name="username" />). On the other hand, if you pass in UserNameView and "gender", the errors are not going to get rendered correctly because the UserNameView isn't concerned with the gender and no input element with a name attribute of "gender" will be found in the DOM under the rendered UserNameView.

    clearErrors: function(view) {
      var inputs = view.$el.find(':input');
      inputs.each(function(index, element) {
        var attr = $(element).attr('name');
        if (!_.isEmpty(attr) && !_.isNull(attr) && !_.isUndefined(attr)) {
          Backbone.Validation.callbacks.valid(view, attr);
          //console.log("Call valid callback to remove all error classes", "attr->", attr, "element", element);
        }
      });
    },

    markErrors: function(view, errors) {
      var firstError = _.first(_.keys(errors));
      var $element = view.$el.find("[name='" + firstError + "']");
      if ($element.length > 0) {
        $element.focus();
      }
      _.each(errors, function(error, attr, list) {
        //console.log("Error: " + "key->" + attr + " value->" + error);
        Backbone.Validation.callbacks.invalid(view, attr, error);
      });
    }

I encourage you to check out the fiddle and let me know if you see any issues or improvements.

wesleymusgrove avatar May 12 '14 15:05 wesleymusgrove